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');
    });
  });
});

Leave a Reply

Your email address will not be published. Required fields are marked *