Learn JavaScript: Novice to Ninja

Chapter 10: Testing and Debugging

Errors and bugs are a fact of life in programming ― they will always be there. A ninja programmer will try to do everything to minimize errors occurring, and find ways to identify and deal with them quickly.

In this chapter, we’ll cover the following topics:

  • Errors, exceptions, and warnings
  • The importance of testing and debugging
  • Strict mode
  • Debugging in the browser
  • Error objects
  • Throwing exceptions
  • Exception handling
  • Testing frameworks
  • Our project ― we’ll add some log messages and tests to the Quiz Ninja application

Errors, Exceptions, and Warnings

Errors are caused when something goes wrong in a program. They are usually caused by one of the following:

  • System error ― there’s a problem with the system or external devices with which the program is interacting.
  • Programmer error ― the program contains incorrect syntax or faulty logic; it could even be as simple as a typo.
  • User error ― the user has entered data incorrectly, which the program is unable to handle.

As programmers, we often have little influence over how external systems work, so it can be difficult to fix the root cause of system errors. Despite this, we should still be aware of them and attempt to reduce their impact by working around any problems they cause. Programmer errors are our responsibility, so we must ensure they are minimized as much as possible and fixed promptly. We also should try to limit user errors by predicting any possible interactions that may throw an error, and ensure they are dealt with in a way that doesn’t negatively affect the user experience. It might even be argued that user errors are in fact also programmer errors, because the program should be designed in a way that prevents the user from making the error.

Exceptions

An exception is an error that produces a return value that can then be used by the program to deal with the error. For example, trying to call a method that is nonexistent will result in a reference error that raises an exception, as you can see in the example below when we try to call the mythical unicorn() function:

unicorn();<< ReferenceError: unicorn is not defined

Stack Traces

An exception will also produce astack trace. This is a sequence of functions or method calls that lead to the point where the error occurred. It’s often not just a single function or method call that causes an error. A stack trace will work backwards from the point at which the error occurred to identify the original function or method that started the sequence. The example below shows how a stack trace can help you find where an error originates from:

function three(){ unicorn(); }function two(){ three(); }function one(){ two(); }one();<< index.html:13 Uncaught ReferenceError: unicorn is not defined    at three (index.html:13)    at two (index.html:17)    at one (index.html:21)    at index.html:24`

In this example, we have three functions: function one() invokes function two() , which then invokes function three() . Function three() then invokes the unicorn() function that doesn’t exist and causes an error. We can use the stack trace to work backwards and see that this error was caused by invoking the function one() in the first place.

Warnings

A warning can occur if there’s an error in the code that isn’t enough to cause the program to crash. This means the program will continue to run after a warning. This might sound good, but it can be problematic, since the issue that produced the warning may cause the program to continue running incorrectly.

An example of a mistake that could cause a warning is assigning a value to a variable that’s undeclared:

pi = 3.142;<< JavaScript Warning: assignment to undeclared variable

Note that not all browsers will display a warning for the code in the example above, so you might not see one if you try it out.

Warnings and exceptions are presented differently in various environments. Some browsers will show a small icon in the corner of the browser window to indicate that an exception or warning has occurred. Others require the console to be open to see any warnings or exceptions.

When a runtime error occurs in the browser, the HTML will still appear, but the JavaScript code will stop working in the background, which isn’t always obvious at first. If a warning occurs, the JavaScript will continue to run (although possibly incorrectly).

The Importance of Testing and Debugging

JavaScript is a fairly forgiving language when it comes to errors; it didn’t implement exceptions at all until ECMAScript version 3. Instead of alerting a user to an error in a program, it just failed silently in the background, and this is sometimes still the case. It might seem like a good idea at first, but the error might give unexpected or incorrect results that nobody spots, or lurk in the background for a long time before causing the program to crash spectacularly. Failing silently makes errors difficult to spot and longer to track down.

For this reason, a ninja programmer should ensure that the code they write fails loudly in development so any errors can be identified and fixed quickly. In production, a ninja programmer should try to make the code fail gracefully (although not completely silently ― we still need to know there’s an error), so the user experience is not affected, if possible. This is achieved by making sure exceptions are caught and dealt with, and code is tested rigorously.

Strict Mode

ECMAScript 5 introduced a strict mode that produces more exceptions and warnings and prohibits the use of some deprecated features. This is due to the fact that strict mode considers coding practices that were previously accepted as just being ‘poor style’ as actual errors.

Increasing the chance of errors might seem like a bad idea at first, but it’s much better to spot errors early on, rather than have them cause problems later. Writing code in strict mode can also help improve its clarity and speed, since it follows conventions and will throw exceptions if any sloppy code practices are used.

Not using strict mode is often referred to as ‘sloppy mode’ as it’s forgiving of sloppy programming practices. Strict mode encourages a better quality of JavaScript to be written that befits a ninja programmer, so its use is recommended.

Strict mode simply requires the following string to be added to the first line of a JavaScript file:

'use strict';

This will be picked up by any JavaScript engine that uses strict mode. If the engine does not support strict mode, this string will simply be ignored.

To see it in action, if you try to assign a value to a variable that is undeclared in strict mode, you’ll get an exception, instead of a warning:

'use strict';e = 2.718;<< ReferenceError: e is not defined

You can even use strict mode on a per-function basis by adding the line inside a function. Strict mode will then only be applied to anything inside that function:

function strictly(){'use strict';// function code goes here

In fact, the recommended way to invoke strict mode is to place all your code into a self-invoking function (covered in more detail in Chapter 12), like so:

(function() {    'use strict';    // All your code would go inside this function}());

Placing 'use strict' at the beginning of a file will enforce strict mode on all the JavaScript in the file. And if you’re using anybody else’s code, there’s no guarantee they’ve coded in strict mode. This technique will ensure that only your code is forced to use strict mode.

Modules and 'use strict'

ES6 introduced JavaScript modules (covered later in chapter 15). These are self-contained pieces of code that are in strict mode by default, so the 'use strict' declaration is not required.

Linting Tools

Linting tools such asJS Lint,JS Hint,andES Lintcan be used to test the quality of JavaScript code, beyond simply using strict mode. They are designed to highlight any sloppy programming practices or syntax errors, and will complain if certain style conventions are not followed, such as how code is indented. They can be very unforgiving and use some opinionated coding conventions, such as not using the ++ and -- increment operators (in the case of JS Lint). Linting tools are also useful for enforcing a programmingstyle guide. This is particularly useful when you are working in a team, as it ensures everybody follows the same conventions.

It’s possible to add a linting tool as a text-editor plugin; this will then highlight any sloppy code as you type. Another option is to use an online linting tool that allows you to simply paste onto a page for feedback. Another option is to install linting software on your computer using npm. This can then be run as part of your workflow.

Passing a lint test is no guarantee that your code is correct, but it will mean it will be more consistent and less likely to have problems.

You can read more about how to use ESLint inthis article on SitePoint.

Feature Detection

Programming in JavaScript can be something of a moving target as the APIs it uses are in a constant state of flux. And there are new APIs being developed as part of the HTML5 specification all the time (more on these in chapter 14). Browser vendors are constantly adding support for these new features, but they don’t always keep up. What’s more, some browsers will support certain features and others won’t. You can’t always rely on users having the most up-to-date browser, either.

The recommended way to determine browser support for a feature is to use feature detection. This is done using an if statement to check whether an object or method exists before trying to actually call the method. For example, say we want to use the shiny new holoDeck API (as far as I know, this doesn’t actually exist … yet), we would wrap any method calls inside the following if block:

if (window.holoDeck) {    virtualReality.activate();}

This ensures that no error occurs if the browser doesn’t support the method, because referencing a nonexistent object such as window.virtualReality will return undefined . As it’s a falsy value, the if block won’t run, but calling the method virtualReality.activate() outside of the if block would cause an exception to be thrown. Feature detection guarantees that the method is only called if it actually exists and fails gracefully, without any exceptions being thrown, if the method doesn’t exist.

Modernizris a library that offers an easy way to implement feature detection andCan I Use?is another useful resource for checking which features are currently supported in different browsers.

The ‘old-school’ way of checking for browser support was known asbrowser sniffing. This involves using the string returned by window.navigator.userAgent property that we met in the last chapter to identify the user’s browser. The relevant methods can then be used for that browser. This approach is not recommended, however, because the user agent string cannot be relied upon to be accurate. Additionally, given the vast array of features you might be developing for, and the shifting nature of support for them across many browsers, this would extremely difficult to implement and maintain.

Debugging in the Browser

Debugging is the process of finding out where bugs occur in the code and then dealing with them. In many cases, the point at which an error occurs is not always where it originated, so you’ll need to run through the program to see what’s happening at different stages of its execution. When doing this, it can be useful to create what are known as breakpoints, which halt the progress of the code and allow us to view the value of different variables at that point in the program. There are a number of options for debugging JavaScript code in the browser.

The Trusty Alert

The most basic form of debugging is to use the alert() method to show a dialog at certain points in the code. Because alert() stops a program from running until OK is clicked, it allows us to effectively put breakpoints in the code that let us check the value of variables at that point to see if they’re what we expect them to be. Take the following example that checks to see if a person’s age is appropriate:

function amIOldEnough(age){    if (age = 12) {        alert(age);        return 'No, sorry.';    } else if (age < 18) {        return 'Only if you are accompanied by an adult.';    }    else {        return 'Yep, come on in!';    }}

The alert method inside the if block will allow us to see the value of the age variable at that point. Once we click on OK, we can then check the function returns the correct message.

If you try the example above, you will find that there is a bug in the code:

amIOldEnough(21)<< 'No, sorry.'

Passing an argument of 21 should result in the string ‘Yep, come on in!’ being returned, but it is returning ‘No, sorry.’ instead. If you tried running the example, you would have seen the alert message show that the value of the variable age is 12 , even though the function was passed an argument of 21 . Closer inspection then reveals a classic mistake has been made. Instead of checking if the value of age is equal to 12 , we have inadvertently assigned it the value of 12 ! To check for equality, we should use === instead of = which assigns a value to a variable (even inside an if block).

In actual fact, we want to return the message ‘No, sorry.’ for all values of age that areless than12, so we could update the code to the following:

function amIOldEnough(age){    if (age < 12) {        alert(age);        return 'No, sorry.';    } else if (age < 18) {        return 'Only if you are accompanied by an adult.';    }    else {        return 'Yep, come on in!';    }}

Try this again and it works as expected:

amIOldEnough(21)<< 'Yep, come on in!'

Using alerts for debugging was the only option in the past, but JavaScript development has progressed since then and their use is discouraged for debugging purposes today.

Using the Console

Most modern JavaScript environments have a console object that provides a number of methods for logging information and debugging. It’s not officially part of the ECMAScript specification, but is well supported in all the major browsers and Node.js.

  • We’ve already seen and used the console.log() method. This can be used to log the value of variables at different stages of the program, although it will not actually stop the execution of the program in the same way as alert() does. For example, we could add some console.log() statements in the amIOldEnough() function, to log the position in the function as well as the value of the age variable:
function amIOldEnough(age){    console.log(age);        if (age < 12) {        console.log(`In the if with ${age}`);        return 'No, sorry.';        } else if (age < 18) {        console.log(`In the else-if with ${age}`);        return 'Only if you are accompanied by an adult.';        } else {        console.log(`In the else with ${age}`);        return 'Yep, come on in!';    }}
  • The console.trace() method will log an interactive stack trace in the console. This will show the functions that were called in the lead up to an exception occurring while the code is running.

This SitePoint postalso lists a few other useful but little-known methods of the console object.

Debugging Tools

Most modern browsers also have a debugging tool that allows you to setbreakpointsin your code that will pause it at certain points. You can then see the values of all the variables at those points and modify them. This can be very useful when trying to track down bugs. Here are the links to the debugger documentation for each of the major browsers:

One of the most useful commands is the debugger keyword. This will create a breakpoint in your code that will pause the execution of the code and allow you to see where the program is currently up to. You can also hover over any variables to see what value they hold at that point. The program can then be restarted by clicking on the ‘play’ button.

The example below shows how the debugger command can be used in the amIOldEnough() function. If you try entering the code below into your browser’s console, then invoke the amIOldEnough() function, the browser’s debugging tool will automatically kick in and you’ll be able see the value of the age variable by hovering over it:

function amIOldEnough(age){    debugger;        if (age < 12) {        debugger;        return 'No, sorry.';        } else if (age < 18) {        debugger;        return 'Only if you are accompanied by an adult.';        } else {        debugger;        return 'Yep, come on in!';    }}amIOldEnough(16);
Using the debugging tool

Remove Debugging Code Prior to Shipping

Remember to remove any references to the debugger command before shipping any code, otherwise the program will appear to freeze when people try to use it!

Error Objects

An error object can be created by the host environment when an exception occurs, or it can be created in the code using a constructor function, like so:

const error = new Error();

This constructor function takes a parameter that’s used as the error message:

const error = new Error('Oops, something went wrong');

There are seven more error objects used for specific errors:

  •  EvalError is not used in the current ECMAScript specification and only retained for backwards compatibility. It was used to identify errors when using the global eval() function.
  •  RangeError is thrown when a number is outside an allowable range of values.
  •  ReferenceError is thrown when a reference is made to an item that doesn’t exist. For example, calling a function that hasn’t been defined.
  •  SyntaxError is thrown when there’s an error in the code’s syntax.
  •  TypeError is thrown when there’s an error in the type of value used; for example, a string is used when a number is expected.
  •  URIError is thrown when there’s a problem encoding or decoding the URI.
  •  InternalError is a non-standard error that is thrown when an error occurs in the JavaScript engine. A common cause of this too much recursion.

These error objects can also be used as constructors to create custom error objects:

const error = new TypeError('You need to use numbers in this function');

All error objects have a number of properties, but they’re often used inconsistently across browsers. The only properties that are generally safe to use are:

  • The name property returns the name of the error constructor function used as a string, such as ‘Error’ or ‘ReferenceError’.
  • The message property returns a description of the error and should be provided as an argument to the Error constructor function.
  • The stack property will return a stack trace for that error. This is a non-standard property and it’s recommended that it is not safe to use in production sites.

Throwing Exceptions

So far we’ve seen errors that are thrown automatically by the JavaScript engine when an error occurs. It’s also possible to throw your own exceptions using the throw statement. This will allow for any problems in your code to be highlighted and dealt with, rather than lurk quietly in the background.

The throw statement can be applied to any JavaScript expression, causing the execution of the program to stop. For example, all the following lines of code will cause a program to halt:

throw 2;throw 'Up';throw { toys: 'out of pram' };

It is best practice, however, to throw an error object. This can then be caught in a catch block, which is covered later in the chapter:

throw new Error('Something has gone badly wrong!');

As an example, let’s write a function called squareRoot() to find the square root of a number. This can be done using the Math.sqrt() method, but it returns NaN for negative arguments. This is not strictly correct (the answer should be an imaginary number, but these are unsupported in JavaScript). Our function will throw an error if the user tries to use a negative argument:

function squareRoot(number) {    'use strict';    if (number < 0) {        throw new RangeError('You can't find the square root of negative numbers')    }    return Math.sqrt(number);};

Let’s test it out:

squareRoot(121);<< 11squareRoot(-1);<< RangeError: You can't find the square root of negative numbers

Exception Handling

When an exception occurs, the program terminates with an error message. This is ideal in development as it allows us to identify and fix errors. In production, however, it will appear as if the program has crashed, which does not reflect well on a ninja programmer.

It is possible to handle exceptions gracefully by catching the error. Any errors can be hidden from users, but still identified. We can then deal with the error appropriately ― perhaps even ignore it ― and keep the program running.

 trycatch , and finally

If we suspect a piece of code will result in an exception, we can wrap it in a try block. This will run the code inside the block as normal, but if an exception occurs it will pass the error object that is thrown onto a catch block. Here’s a simple example using our squareRoot() function from earlier:

function imaginarySquareRoot(number) {    'use strict';    try {        return String(squareRoot(number));    } catch(error) {        return squareRoot(-number)+'i';    }}

The code inside the catch block will only run if an exception is thrown inside the try block. The error object is automatically passed as a parameter to the catch block. This allows us to query the error name, message and stack properties, and deal with it appropriately. In this case, we actually return a string representation of an imaginary number:

imaginarySquareRoot(-49) // no error message shown<< '7i'

finally block can be added after a catch block. This will always be executed after the try or catch block, regardless of whether an exception occurred or not. It is useful if you want some code to run in both cases. We can use this to modify the imaginarySquareRoot() function so that it adds “+ or -” to the answer before returning it:

function imaginarySquareRoot(number) {    'use strict';    let answer;    try {        answer = String(squareRoot(number));    } catch(error) {        answer = squareRoot(-number)+"i";    } finally {        return `+ or - ${answer}`;    }}

You can read more about how to handle errors effectively inthis article on SitePoint.

Tests

Testing is an important part of programming that can often be overlooked. Writing good tests means your code will be less brittle as it develops, and any errors will be identified early on.

A test can simply be a function that tests a piece of code runs as it should. For example, we could test that the squareRoot() function that we wrote earlier returns the correct answer with the following function:

function itSquareRoots4() {    return squareRoot(4) === 2;}

Here we’re comparing the result of squareRoot(4) with the number 2 . This will return true if our function works as expected, which it does:

itSquareRoots4();<< true

This is in no way a thorough test of the function – it might just be a fluke that the function returns 2 , and this might be the only value it works for. It does, however, demonstrate how you would start to implement tests for any functions that are written.

Test-driven Development

Test-driven development(TDD) is the process of writing tests before any actual code. Obviously these tests will initially fail, because there is no code to test. The next step is to write some code to make the tests pass. After this, the code is refactored to make it faster, more readable, and remove any repetition. The code is continually tested at each stage to make sure it continues to work. This process should be followed in small piecemeal chunks every time a new feature is implemented, resulting in the following workflow:

  1. Write tests (that initially fail)
  2. Write code to pass the tests
  3. Refactor the code
  4. Test refactored code
  5. Write more tests for new features

This is often referred to as the “red-green-refactor” cycle of TDD, as failing tests usually show up as red, and tests that pass show as green.

Test-driven development is considered to be best practice, but in reality you’ll find most developers tend to be more pragmatic when it comes to writing tests. The TDD mindset can be hard to always use, and at the end of the day, any tests are better than no tests at all. In fact,this post by David Heinemeier Hanssonshows that even the very best coders don’t always use TDD, and make no apologies for not doing so. We cover an example in this chapter, but won’t be using it for the most of the examples in the rest of the book as it would make it far too long!

Testing Frameworks

It is possible to write your own tests, as we saw earlier with the itSquareRoots4() test function, but this can be a laborious process. Testing frameworks provide a structure to write meaningful tests and then run them. There are a large number of frameworks available for JavaScript, but we’ll be focusing on the Jest framework.

Jest

Jestis a TDD framework, created by Facebook, that has gained a lot of popularity recently. It makes it easy to create and run tests by providing helper methods for common test assertions.

To use Jest, first we need to install it using npm . Enter the following command in a terminal:

npm install -g jest

This should install Jest globally. To check everything worked okay, try running the following command to check the version number that has been installed:

jest -v<< v19.0.2

The version number might be different on your install, but if it returns a value, it means Jest is installed correctly.

Next we’ll create an example test to see if it works. Let’s write a test to see if our squareRoot() function from earlier works. Create a file called squareRoot.test.js and add the following code:

function squareRoot(number) {    'use strict';    if (number < 0) {        throw new RangeError("You can't find the square root of negative numbers")    }    return Math.sqrt(number);};test('square root of 4 is 2', () => {expect(squareRoot(4)).toBe(2);});

This file contains the squareRoot() function that we are testing, as well as a test() function. The first parameter of the test() function is a string that describes what we are testing, in this case that ‘square root of 4 is 2’. The second parameter is an anonymous function that contains a function called expect() , which takes the function we’re testing as an argument, and returns anexpectation object. The expectation object has a number of methods calledmatchers. In the example above, the matcher is toBe() , which tests to see if the value returned by our squareRoot() function is the same as the value provided as an argument (2, in this case). These matchers are named so they read like an English sentence, making them easier to understand (even for non-programmers), and the feedback they provide more meaningful. The example above almost reads as ‘expect the square root of 4 to be 2’. It’s important to recognize that these are just functions at the end of the day, so they behave in exactly the same way as any other function in JavaScript. This means that any valid JavaScript code can be run inside the test function.

To run this test, simply navigate to the folder that contains the file squareRoot.test.js and enter the following command:

jest -c {}

This will run all files that end in ‘test.js’ within that folder. The -c {} flag at the end is shorthand for ‘configuration’. We don’t need any extra configuration, so we simply pass it an empty object.

If everything is working okay, it should produce the following output:

<< PASS  ./squareRoot.test.js✓ square root of 4 is 2 (2ms)Test Suites: 1 passed, 1 totalTests:       1 passed, 1 totalSnapshots:   0 totalTime:        2.996s

Hooray! This tells us that there was 1 test and it passed in a mere 2ms!

Crunching Some Numbers

To demonstrate the TDD process, we’ll have a go at creating a small library called ‘Number Cruncher’ that will contain some functions that operate on numbers. The first function we’ll try to implement will be called factorsOf() . This will take a number as a parameter and return all the factors (The factors, or divisors, of a number are any integers that divide exactly into the number without leaving a remainder. For example, the factors of 6 are 1, 2, 3 and 6.) of that number as an array.

Since we’re doing TDD, we need to start by writing the tests first, so create a file called numberCruncher.test.js and add the following code:

test('factors of 12', () => {    expect(factorsOf(12)).toEqual([1,2,3,4,6,12]);});

Use of toEqual()

We have used the toEqual() match in this test. This is because we are testing an array.

This test says our factorsOf() function should return an array containing all the factors of 12 in order, when 12 is provided as an argument. If we run this test, we can see that it fails spectacularly:

jest -c {}<< FAIL  ./numberCruncher.test.js● factors of 12    ReferenceError: factorsOf is not defined    at Object.<anonymous>.test (numberCruncher.test.js:2:10)    at process._tickCallback (internal/process/next_tick.js:103:7)✕ factors of 12 (6ms)Test Suites: 1 failed, 1 totalTests:       1 failed, 1 totalSnapshots:   0 totalTime:        1.424s

Well, what did you expect? We haven’t written any code yet! Let’s have a go at writing the factorsOf() function. Add the following to the top of the numberCruncher.test.js file:

'use strict';function factorsOf(n) {const factors = [];    for (let i=1; i < n ; i++) {        if (n/i === Math.floor(n/i)){        factors.push(i);        }}    return factors;}

This function creates a local variable called factors and initializes it as an empty array. It then loops through every integer value from 1 up to n (the number that was given as an argument) and adds it to the array of factors using the push() method, if it’s a factor. We test if it’s a factor by seeing if the answer leaves a whole number with no remainder when n is divided by the integer 1 (the definition of a factor).

This Isn’t Totally Realistic

To make things easier in this example, we’re putting the code into the same file as the tests, but in reality you’d usually keep them in separate files.

Try running the test again:

jest -c {}<< FAIL  ./numberCruncher.test.js● factors of 12    expect(received).toBe(expected)    Expected value to be (using ===):    [1, 2, 3, 4, 6, 12]    Received:    [1, 2, 3, 4, 6]    Difference:    - Expected    + Received    @@ -2,7 +2,6 @@    1,    2,    3,    4,    6,    -  12,    ]    at Object.<anonymous>.test (numberCruncher.test.js:14:25)    at process._tickCallback (internal/process/next_tick.js:103:7)✕ factors of 12 (12ms)Test Suites: 1 failed, 1 totalTests:       1 failed, 1 totalSnapshots:   0 totalTime:        0.801s, estimated 1sRan all test suites.

Oh dear, it still failed. This time, the failure message is a bit more specific. It says it was expecting the array [1,2,3,4,6,12] but received the array [1,2,3,4,6] ― the last number 12 is missing. Looking at our code, this is because the loop only continues while i < n . We need i to go all the way up to and including n , requiring just a small tweak to our code:

function factorsOf(n) {    const factors = [];    for (let i=1; i <= n ; i++) { // change on this line        if (n/i === Math.floor(n/i)){        factors.push(i);        }    }    return factors;}

Now if you run the test again, you should get a nice message confirming that our test has passed:

jest -c {}<< PASS  ./numberCruncher.test.js✓ factors of 12 (7ms)Test Suites: 1 passed, 1 totalTests:       1 passed, 1 totalSnapshots:   0 totalTime:        3.012sRan all test suites.

Our test passed, but this doesn’t mean we can stop there. There is still one more step of the TDD cycle: refactoring.

There are a few places where we can tidy up the code. First of all, we should only really be testing for factors up to the square root of the number, because if i is a factor, then n/i will be a factor as well. For example, if you’re trying to find the factors of 36, when we test if 2 is a factor, we see that it divides into 36, 18 times exactly. This means that 2 is a factor, but it also means 18 is as well. So we’ll start to find factors in pairs, where one is below the square root of the number, and the other is above the square root. This will have the effect of reducing the number of steps in the for loop dramatically.

Secondly, the test to see if i is a factor of n can be written more succinctly using the % operator. If i is a factor of n , then n%i will equal 0 because there’s no remainder.

We’ll also need to sort the array at the end of the function, because the factors are not added in order any more. We can do this using the sort() method with a callback that we saw in Chapter 4.

Let’s refactor our code in numberCruncher.test.js to the following:

function factorsOf(n) {    const factors = [];    for (let i=1 , max = Math.sqrt(n); i <= max ; i++) {        if (n%i === 0){        factors.push(i,n/i);        }    }    return factors.sort((a,b) => a - b);}

Now run the test again to confirm it still passes:

jest -c {}<< PASS  ./numberCruncher.test.js✓ factors of 12 (8ms)Test Suites: 1 passed, 1 totalTests:       1 passed, 1 totalSnapshots:   0 totalTime:        1.615sRan all test suites.

Now our tests are passing, and our code has been refactored, it’s time to add some more functionality. Let’s write another function called isPrime() that will return true if a number is prime and false if it isn’t. Let’s start by writing a couple of new tests for this at the end of numberCruncher.test.js :

test('2 is prime', () => {    expect(isPrime(2)).toBe(true);});test('10 is not prime', () => {    expect(isPrime(10)).not.toBe(true);});

The first test checks whether true is returned when a prime number ( 2 ) is provided as an argument, and another to check that true is not returned if a non-prime number ( 10 ) is given as an argument. These tests use the toBe() matcher to check if the result is true . Note the nice use of negation using the not matcher (although we should probably be checking if it’s false because this test will pass if anything but true is returned).

More Matcher Methods

You can see a full list ofJest’s matcher methods here.

If you run the tests again, you’ll see that our new tests are failing, and our factors test is still passing:

jest -c {}<< FAIL  ./numberCruncher.test.js● 2 is prime    ReferenceError: isPrime is not defined    at Object.<anonymous>.test (numberCruncher.test.js:18:10)    at process._tickCallback (internal/process/next_tick.js:103:7)● 10 is not prime    ReferenceError: isPrime is not defined    at Object.<anonymous>.test (numberCruncher.test.js:22:10)    at process._tickCallback (internal/process/next_tick.js:103:7)✓ factors of 12 (4ms)✕ 2 is prime (1ms)✕ 10 is not prime (1ms)Test Suites: 1 failed, 1 totalTests:       2 failed, 1 passed, 3 totalSnapshots:   0 totalTime:        0.812s, estimated 1sRan all test suites.

This is to be expected, since we’re yet to write any code for them.

We’d better write the isPrime() function. This will use the factorsOf() function and check to see if the number of factors in the array returned by the factorsOf() function is 2. This is because all prime numbers have precisely two factors. Add the following code to the bottom of the numberCruncher.test.js file:

function isPrime(n) {return factorsOf(n).length === 2;}

Now if we run the tests again, we can see that all three of our tests have passed:

jest -c {}<< PASS  ./numberCruncher.test.js✓ factors of 12 (6ms)✓ 2 is prime (1ms)✓ 10 is not prime (1ms)Test Suites: 1 passed, 1 totalTests:       3 passed, 3 totalSnapshots:   0 totalTime:        2.853sRan all test suites.

Our library of functions is growing! The next step is to again refactor our code. It’s a bit brittle at the moment, because both functions accept negative and non-integer values, neither of which are prime. They also allow non-numerical arguments to be provided. It turns out that the factorsOf() function fails silently and returns an empty array if any of these are passed to it. It would be better to throw an exception to indicate that an incorrect argument has been used. Let’s create some tests to check that this happens. Add the following tests to the numberCruncher.test.js file:

it('should throw an exception for non-numerical data', () => {    expect(factorsOf('twelve').toThrow();});it('should throw an exception for negative numbers', () => {    expect(() => factorsOf(-2)).toThrow();});it('should throw an exception for non-integer numbers', () => {    expect(() => factorsOf(3.14159)).toThrow();});

These tests all use the toThrow() method to check that an exception has been thrown if the wrong data is entered as an argument.

While we’re at it, we can add some extra tests so the isPrime() function also deals with any incorrect arguments. No exceptions are necessary in these cases; non-numerical data, negative numbers and non-integers are simply not prime, so the function should just return false . Add the following code to the bottom of the numberCruncher.test.js file:

test('non-numerical data returns not prime', () => {    expect(isPrime('two')).toBe(false);});test('non-integer numbers return not prime', () => {    expect(isPrime(1.2)).toBe(false);});test('negative numbers return not prime', () => {    expect(isPrime(-1)).toBe(false);});

If you run the tests again, you’ll see that the new tests for the factorsOf() function fail as expected, but the new tests for the isPrime() function actually pass. This is a happy accident because the factorsOf() function is returning an empty array, which is conveniently not of length 2 , so false is returned by the function anyway.

Let’s try and make all the tests pass by throwing some exceptions in the factorsOf() function. Change the factorsOf() function to the following in numberCruncher.test.js :

function factorsOf(n) {    if(Number.isNaN(Number(n))) {        throw new RangeError('Argument Error: Value must be an integer');    }    if(n < 0) {        throw new RangeError('Argument Error: Number must be positive');    }    if(!Number.isInteger(n)) {        throw new RangeError('Argument Error: Number must be an integer');    }    const factors = [];    for (let i=1 , max = Math.sqrt(n); i <= max ; i++) {        if (n%i === 0){        factors.push(i,n/i);        }    }    return factors.sort((a,b) => a - b);}

Now the function checks to see if a negative number or non-integer has been provided as an argument, and throws an exception in both cases. Let’s run our tests again:

jest -c{}<< FAIL  ./numberCruncher.test.js● non-numerical data returns not prime    RangeError: Argument Error: Value must be an integer    at factorsOf (numberCruncher.test.js:5:11)    at isPrime (numberCruncher.test.js:23:10)    at Object.<anonymous>.test (numberCruncher.test.js:57:10)● non-integer numbers return not prime    RangeError: Argument Error: Number must be an integer    at factorsOf (numberCruncher.test.js:11:11)    at isPrime (numberCruncher.test.js:23:10)    at Object.<anonymous>.test (numberCruncher.test.js:61:10)● negative numbers return not prime    RangeError: Argument Error: Number must be positive    at factorsOf (numberCruncher.test.js:8:11)    at isPrime (numberCruncher.test.js:23:10)    at Object.<anonymous>.test (numberCruncher.test.js:65:10)✓ Returns factors of 12 (4ms)✓ 2 is prime (1ms)✓ 10 is not prime✓ Exception for non-numerical data✓ Exception for negative numbers (1ms)✓ Exception for non-integer numbers✕ Non-numerical data returns not prime (2ms)✕ Non-integer numbers return not prime✕ Negative numbers return not prime (1ms)Test Suites: 1 failed, 1 totalTests:       3 failed, 6 passed, 9 totalSnapshots:   0 totalTime:        3.516sRan all test suites.

Oh, no! Our tests for the factorsOf() function all pass… but the exceptions have caused the isPrime() function to choke and fail the tests. We need to add code that handles any exceptions that might be thrown when the factorsOf() function is called from within the isPrime() function. This sounds like a job for a try and catch block! Change the isPrime() function in the numberCruncher.test.js file to the following:

function isPrime(n) {try{    return factorsOf(n).length === 2;} catch(error) {    return false;}}

Now we’ve placed the original code inside a try block, so if factorsOf() throws an exception, we can pass it on to the catch block and handle the error. All we have to do here is simply return false if an error is thrown.

Now we’ll run our tests again, and hopefully you’ll see the following message:

jest -c{}<< PASS  ./numberCruncher.test.js✓ Returns factors of 12 (4ms)✓ 2 is prime (1ms)✓ 10 is not prime✓ Exception for non-numerical data (1ms)✓ Exception for negative numbers✓ Exception for non-integer numbers (1ms)✓ Non-numerical data returns not prime✓ Non-integer numbers return not prime (1ms)✓ Negative numbers return not primeTest Suites: 1 passed, 1 totalTests:       9 passed, 9 totalSnapshots:   0 totalTime:        2.381sRan all test suites.

Hooray! All our tests are now passing. We’ll stop there, but hopefully this demonstrates how TDD can be used to keep adding functionality in small increments using the fail, pass and refactor cycle.

Quiz Ninja Project

We’re now going to use the console.log() method to log when some of the important functions are called. This will help to make our code in the Quiz Ninja project easier to debug. The main functions in the game are all methods of the game object: game.start()game.ask()game.check(event) , and game.gameOver() . Add the following lines of code to the beginning of the relevant functions:

console.log('start() invoked');console.log('ask() invoked');console.log('check(event) invoked');console.log('gameOver() invoked');

These declarations will log a message in the console when each method is invoked, so we can see where the program is in its runtime. There will be no impact on the player, though, as we’re just using the console.

Try playing the game with the console open in the browser. You should see the messages logged in the console as the program runs, as in the screenshot shown below:

Playing Quiz Ninja with the console open

You can see a live example onCodePen.

Chapter Summary

  • Bugs are unavoidable in code, and it’s best to find them early rather than later.
  • JavaScript can be put into strict mode using the string "use strict" . This can be used in a whole file or just a single function.
  • Linting tools can be used to ensure your code follows good practice and conventions.
  • Feature detection can check whether a method is supported before calling it, helping to avoid an exception being thrown.
  • The console and browser’s built-in debugging tool can be used to interactively find and fix bugs in code.
  • Exceptions can be thrown using the throw statement.
  • An error object is created when an exception occurs.
  • Any code placed inside a try block will pass any error objects to a catch block when an exception occurs. Any code inside a finally block will run if an exception does or does not occur.
  • Test-driven development is the practice of writing tests that fail, then writing the code that passes the test, then refactoring the code every time a new feature is implemented.
  • The Jest framework can be used to test your code.

In the next chapter, we’ll be taking our understanding of functions to the next level and trying out some functional programming techniques in JavaScript.

Pages: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16