Writing Unit Tests to Handle Unexpected Inputs

We’re picking up from my last post about getting started with unit testing. Now, let’s take a look at how to write tests for unexpected inputs that you might run across and how tests can document your modules.

One of the benefits of unit testing is that it forces you to be clear about what your module is doing and what outcomes you expect for different inputs. Let’s bring those benefits to our adding-machine.js file. Here’s where it should be right now:

function addingMachine (number1, number2) {
  return (number1 + number2);
}
module.exports =  addingMachine;

The problem here is that we don’t know what to expect if there’s input that you wouldn’t expect. What should our adding machine do if it’s given a Boolean, a symbol or an object as input? We could see what JavaScript would give us and then test for that, but that’s lazy: it doesn’t force us to consider what a user of the module would want and write the program appropriately. In this case, I want my adding machine to only take number types and also throw an error if passed NaN (not a number, which is technically a number if you look at it with typeof). So, let’s write a test that will test to see if adding machine will throw an error if passed NaN:

  describe ('addingMachine Error Handling', function () {
    it ('should throw if passed a non-number (NaN)', function () {
      assert.throws(addingMachine.bind(null, NaN, 5), Error, /passed NaN/, 'NaN input, wrong error message');
  });
});

Let’s talk about what we’re doing here. The assert.throws function expects a function as input. Because of this, we can’t put addingMachine(NaN, 5) in as an argument, it just returns its output. If it throws while it’s running, it won’t register for your test. That’s why we need to use bind. bind basically creates a new function with its arguments hard-coded to the inputs given with the this context passed as the first argument. In other words, addingMachine.bind(null, NaN, 5) creates a function that has number1 hard-coded to NaN and number2 set to 5. We don’t have to worry about the this context because we’re not using this in the function. We can just set it to null. The second argument, /passed NaN/ is regex for the error that it expects. The third input, 'NaN input, wrong error message' is the error that will be given if it’s the wrong error message.

Let’s run the test. With npm test, you’ll see that it fails. That’s because we haven’t re-written addingMachine yet. Let’s do that:

function addingMachine (number1, number2) {
  if (Number.isNaN(number1) || Number.isNaN(number2)) throw new Error ('passed NaN');
  return (number1 + number2);
}
module.exports =  addingMachine;

You don’t need to be as verbose as I in your error handling, but you get the idea. Now, if you re-run your test with npm test, it will pass.
As you probably know, there are 7 different data types in JavaScript:
– Boolean
– Null
– Undefined
– Number
– String
– Symbol
– Object
There’s only one of those types that we want addingMachine to deal with: Number. Let’s put in tests so that we will expect an error if one of the other types is thrown:

    it ('should throw if passed a string', function () {
      assert.throws(addingMachine.bind(null, '5', 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed undefined', function () {
      assert.throws(addingMachine.bind(null, 5, undefined), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed null', function () {
      assert.throws(addingMachine.bind(null, null, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed an object', function () {
      assert.throws(addingMachine.bind(null, {}, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed a boolean', function () {
      assert.throws(addingMachine.bind(null, true, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed a symbol', function () {
      const mySymbol = Symbol();
      assert.throws(addingMachine.bind(null, mySymbol, 5), /passed non-number/, 'wrong error message');
    });

Run your npm test. It should fail because you haven’t written the throw for those issues.

Let’s update addingMachine to throw whenever we get one of the other types:

function addingMachine (number1, number2) {
  if (Number.isNaN(number1) || Number.isNaN(number2)) throw new Error ('passed NaN');
  if (typeof number1 !== 'number' || typeof number2 !== 'number') throw new Error ('passed non-number');
  return (number1 + number2);
}
module.exports =  addingMachine;

If you run your tests now, they should all pass.

I encourage you to test all different types of inputs in all the tests you write as well as unique inputs of the types you expect. Your tests become a self-documenting way of showing future maintainers of your code how your program is expected to work. It will also clarify for you what your code is doing and put you in a place where you’re more familiar with what’s going on and why.

One thing you might have noticed this time is that our way of writing code might have seemed backwards this time: we wrote tests and then we made them pass. This is a technique called “Test Driven Development” or TDD. It’s a way of developing new code that puts an emphasis on writing tests for what you want a small amount of code to do and then writing only just enough to get it to pass. It offers other benefits as well. We’ll explore TDD next time. In the meantime, here are the final states that your two files should have ended up in after these two introductory posts on unit testing:
adding-machine.js:

function addingMachine (number1, number2) {
  if (Number.isNaN(number1) || Number.isNaN(number2)) throw new Error ('passed NaN');
  if (typeof number1 !== 'number' || typeof number2 !== 'number') throw new Error ('passed non-number');
  return (number1 + number2);
}
module.exports =  addingMachine;

test.js:

const assert = require('assert');
const addingMachine = require('./adding-machine');

describe ('addingMachine', function () {
  it ('should return 2 as the sum of 1 and 1', function () {
      assert.equal(2, addingMachine(1, 1));
  });
  it('should return 11 as the sum of 3 and 8', function () {
    assert.equal(11, addingMachine(3, 8));
  });
  it ('should handle zero properly, returning 1 as the sum of 0 and 1', function () {
      assert.equal(1, addingMachine(0, 1));
  });
  it ('should handle negative numbers properly, returning 5 as the sum of 10 and -5', function () {
      assert.equal(5, addingMachine(10, -5));
  });
  describe ('addingMachine Error Handling', function () {
    it ('should throw if passed a non-number (NaN)', function () {
      assert.throws(addingMachine.bind(null, NaN, 5), /passed NaN/, 'NaN input, wrong error message');
    });
    it ('should throw if passed a string', function () {
      assert.throws(addingMachine.bind(null, '5', 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed undefined', function () {
      assert.throws(addingMachine.bind(null, 5, undefined), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed null', function () {
      assert.throws(addingMachine.bind(null, null, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed an object', function () {
      assert.throws(addingMachine.bind(null, {}, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed a boolean', function () {
      assert.throws(addingMachine.bind(null, true, 5), /passed non-number/, 'wrong error message');
    });
    it ('should throw if passed a symbol', function () {
      const mySymbol = Symbol();
      assert.throws(addingMachine.bind(null, mySymbol, 5), /passed non-number/, 'wrong error message');
    });
  });
});

Getting started with Unit Testing

There are lots of different ways to get started with unit testing in JavaScript. Today, we’ll look at a simple way to get started: Mocha, a popular unit testing engine. We’ll just look at the vanilla version of Mocha (I suppose the black version would be a more appropriate metaphor in this case). You can make your test cases easier to read by making them read more like an English sentence with packages like Chai, but we’ll leave that for another day.

Prerequisites for this tutorial:
– Node is installed on your machine (how to install node)
– NPM is installed on your machine (should come with Node)

Go ahead and create a directory in your root directory (C:\ or ~) or your code directory named unit-testing. Create a file called test.js Do an npm install mocha there. Then npm init, putting in whatever values you like for the name. Make sure that the test command is mocha

Now, run npm test. You should see something like this:

Tests passing because there are no tests!

Okay, it’s passing, but that’s because we don’t have any tests. Let’s fix that. We’ll create a simple adding machine that will tell us the sum of two numbers. A trivial task, but we’re here to learn unit testing, not show off our amazing JS skills. Create a file named adding-machine.js and put in this code:

function addingMachine (number1, number2) {
  return (number1 + number2);
}
// We need to export this so our test file can see it, so add this also:
module.exports =  addingMachine;

Now, switch over to your test.js file and let’s add some test. First, you’ll want to add the assert library, which is how you’ll tell mocha what to expect: const assert = require('assert');. Then, you’ll require the program you’re testing: const addingMachine = require('./adding-machine');. Now, let’s actually add some tests.

We’ll start with a describe, which is a way to group your tests:

describe('addingMachine', function() {
  // your tests will go here
});

This is so that your tests neatly fall under the describe and when reading output you can see that those tests are describing the adding machine. It might not seem that important, but if you’re testing a whole bunch of modules at the same time (say, in your build system), having a well-structured test suite is a great benefit. Okay, now let’s actually write a test:

it('should return 2 as the sum of 1 and 1', function () {
  assert.equal(2, addingMachine(1, 1));
});

When we use equal, we’re telling mocha to expect that two inputs of the equal function should be equal. (Wow, I’m sure you’d never have guessed!) So, in this case, we’re saying that the results of putting 1 and 1 into the adding machine should equal the first argument, 2.
Now run npm test and you should see this:

Sum 1 and 1 test passing

Great! The test passed. If yours didn’t pass, check to make sure that your code matches mine. You can also use console.log in both the test.js file and the adding-machine.js file to check your inputs and arguments.

This is a fine test case, but it’s not enough. If the code for the adding machine had been return (number1 + number1), this test still would have passed, so it’s important that we check inputs that are different. Let’s do it!

it('should return 11 as the sum of 3 and 8', function () {
  assert.equal(11, addingMachine(3, 8));
});

Run npm test again, it should pass. We should also check numbers that have special properties to make sure that addingMachine handles them the way we expect:

  it ('should handle zero properly, returning 1 as the sum of 0 and 1', function () {
      assert.equal(1, addingMachine(0, 1));
  });
  it ('should handle negative numbers properly, returning 5 as the sum of 10 and -5', function () {
      assert.equal(5, addingMachine(10, -5));
  });

Now, we’ve got a good test suite going. Here’s where your files should be at this point:
test.js:

const assert = require('assert');
const addingMachine = require('./adding-machine');

describe ('addingMachine', function () {
  it ('should return 2 as the sum of 1 and 1', function () {
      assert.equal(2, addingMachine(1, 1));
  });
  it('should return 11 as the sum of 3 and 8', function () {
    assert.equal(11, addingMachine(3, 8));
  });
  it ('should handle zero properly, returning 1 as the sum of 0 and 1', function () {
      assert.equal(1, addingMachine(0, 1));
  });
  it ('should handle negative numbers properly, returning 5 as the sum of 10 and -5', function () {
      assert.equal(5, addingMachine(10, -5));
  });
});

adding-machine.js:

function addingMachine (number1, number2) {
  return (number1 + number2);
}

Next time, we’ll take a look at how to expand our test suite and handle unexpected inputs.

Why Unit test in JavaScript?

Recently at my work there has been a push to do more unit tests on the front end. There’s also been a lot of pushback: “Why do we have to unit test?” “This just takes more of our time that we could be using for feature work,” “Our automated UI tests will catch problems,” “Our QA testers are great, they’ll catch any bugs,” and so on.

Most of these arguments seem like compelling reasons to not unit test. Isn’t that just something that Java programmers do? Actually, unit testing in your JavaScript can save you time and headaches. Not only that, but you can look like a hero to your coworkers by reducing the number of test cases that they have to cover, either with manual tests or automated UI tests.

Not only that, but you’ll be a hero for whoever has to refactor or add things to the code that you’ve covered with good unit tests. There are few things more terrifying than having to go into old, crusty code to refactor it for performance or add features to it if there are no unit tests. If you break something, how will you know? Who knows how many downstream things depend on this code and its obtuse output? Will your testers (if you have any) catch things or will they not have time to do as comprehensive a regression check as this needs? Or will your customers come screaming complaining of a broken website, or worse, will they disappear without a peep?

You’ll also have a body of work that you can thank yourself for later: there are few better feelings than having confidence from good unit tests that you can refactor the code until it’s a beautiful shining example of clean code that would make a seasoned developer weep with joy. Wait, it still works, right? Well, the good unit tests pass, so we’re good to go.

Well-written unit tests can produce results that seem impossible for such a simple thing. It gives confidence to you that your code is working at a basic level. It gives confidence to other developers on your team that you’re looking out for their sanity and that you’re likely writing maintainable code that isn’t adding a ton of technical debt to the project. It gives confidence to QA that you’ve covered the basic scenarios and handled edge cases so that they don’t have to. It also checks a box that many in management are looking for and improves code coverage metrics.

There’s a huge number of reasons to unit test, but how do you get started? We’ll look at that next time.