How to Use Data In Unit Tests

One of the challenges of unit testing is checking expected outputs based on certain inputs.

One possible solution to this issue is to just hard code your inputs and expected outputs into the test file. This brings some problems with it:

Why Hard-coding Data isn’t a Good Approach

  • Repeated code

    If you’re testing multiple aspects of some code, all with the same complicated input, you can end up with a mess like this:

    describe('friend contact', function () {
    it('will not throw when passed an valid data', function () {
      assert.doesNotThrow(friendContact.bind(null, [{ firstName: 'Clive', lastName: 'Lewis', phone: '44 01223 332129', lastContact: '2016-11-29', }, { firstName: 'Martin', lastName: 'Chemnitz', phone: '49 345 5520', lastContact: '2016-11-09',}]));
    });
    describe('returned output', function() {
      it('returns an object when passed valid data', function () {
        assert.equal('object', typeof friendContact([{ firstName: 'Clive', lastName: 'Lewis', phone: '44 01223 332129', lastContact: '2016-11-29', }, { firstName: 'Martin', lastName: 'Chemnitz', phone: '49 345 5520', lastContact: '2016-11-09', }]));
      });
      it('returns an object with the expected data', function () {
        assert.deepStrictEqual({ firstName: 'Martin', lastName: 'Chemnitz', phone: '49 345 5520', lastContact: '2016-11-09',}, friendContact([{ firstName: 'Clive', lastName: 'Lewis', phone: '44 01223 332129', lastContact: '2016-11-29', }, { firstName: 'Martin', lastName: 'Chemnitz', phone: '49 345 5520', lastContact: '2016-11-09', }]));
      });
    });
    });
    

    Bleh! All that repeated test data is a headache. Even with copy-paste, there’s still the potential for error and makes debugging the test hard for you and even worse for future-you (or the next poor soul that deals with it).

  • Unreadable Tests

    You might think that the readability problem in the example above could just be solved by better formatting:

    describe('friend contact', function () {
      it('will not throw when passed an valid data', function () {
        assert.doesNotThrow(friendContact.bind(null, [
          {
            firstName: 'Clive',
            lastName: 'Lewis',
            phone: '44 01223 332129',
            lastContact: '2016-11-29',
          }, {
            firstName: 'Martin',
            lastName: 'Chemnitz',
            phone: '49 345 5520',
             lastContact: '2016-11-09',
           }
         ]));
      });
      //snip, you  get the idea
    

    Now we’ve not only got the repeated code problem, we’re also left to wonder where the data stops and the test code begins. Again, this is a maintenance nightmare.

  • Difficult to refactor

    If your API provider decides to change the data structure, do you want to be the one that refactors the tests after you update your program to deal with the new data? Not only that, it’s harder to spot data input and output errors with test code and data mixed

If you hard code data into the test file, you’re in for a world of hurt. There’s a better way, let’s look at it.

How to Separate Test data

It’s not difficult to take the data out of the unit tests to make it more maintainable:

  1. Create a separate file from your test-foo.spec.js (or whatever your test file is) and call it something like test-foo-data.js. (I like to create a separate folder inside my test folder called test-data and put my data in there.)

  2. In your test file, create your data, assign it to a const and export each const so it can be used. (An alternative approach is to create and export an object with each property a data input that you’d like to run through your unit test.)

You’ll want something like this:

const longString = 'Supercalifragilisticexpialidocious';
const longStringVowelCount = 16;

module.exports = {
  longString,
  longStringVowelCount,
};

Now, you can bring it into your test file like this:

const testData = require('./test-data/test-foo-data'); // or whatever path there is to your data

And use it like this:

assert.equal(countVowelFunction(testData.longString), testData.longStringVowelCount);

Where to Get Your data

You might ask, ‘Wait a minute Nicholas, this sounds great, but where does the data come from?’

Good question. There’s a few different places you can get your data.

  • Copy data from production or the API

    This is probably the best option. If the thing you’re going to be taking the data from is already running, just copy some of the data that you’ll be using as input. This assures that the data is formatted in the same way that you’ll get when you’re up and running.

    The drawback to this approach is if the API or production is buggy, once it’s fixed, your code won’t work anymore. This isn’t that much of an issue, if your data is buggy, you can’t really call any code based on it complete.

  • Write data based on the documentation that you’ve been given

    If you’ve been given documentation for the system that you’ll be getting your data from, go ahead and model your test data on it. Unfortunately, not all documentation matches actual program behavior, so be prepared to check that your mock data matches the API.

  • Write data based on the spec that the backend is using to develop

    If the backend isn’t complete yet and you need to do parallel development for whatever reason (time crunch, team resources, or other wackiness), you can write your unit test data to be based on whatever specification the backend developers are writing to. Weather your team uses user stories or some other way of storing specifications, just base your test data on that.

  • Write data based on what makes sense to you

    If you’re the one doing the backend development, you can just write test data that seems reasonable based on what you plan to output. This has the added benefit of forcing you to consider the best way to structure your data for the front end and develop the back end accordingly.

That’s how you can use test data in your unit tests! It’s not that hard once you see how to do it. Next time, we’ll talk about another way to make your tests more readable: using Chai, a way of making assertions that more closely resembles an English sentence.

Starting Test Driven Development with Mocha

How does TDD work?

Last time in our introduction to unit testing, we touched on TDD. Let’s talk about TDD and write a simple program that illustrates the steps.

The basic principal of TDD is to write your tests first and then write code to make the tests pass. Not only that, but in TDD, you write the smallest test that you can and the smallest amount of code to make that test pass. By doing this, you won’t end up with an irrelevant test suite that takes forever to write the code for. Also, you’ll know that your tests are actually testing properly: Especially when you first start writing tests, they may not work. By making sure that they fail and then pass, you’re making it more likely that you’re actually testing what you need to.

Even though we’re writing one test at a time, you’ll want to think of the design of the program before you start writing the tests so that you’re headed in the right direction and don’t have to do a bunch of refactoring. (Refactoring is a step of TDD, but you don’t want to take all your time doing it.)

So today, let’s write a simple program that will help you stay in touch with your friends. It will take a JSON input of a data structure of your friends with their name, a way to contact them, and date that you last contacted/were contacted by them. It will then return an object with the friend’s information that you talked to the longest ago.
Let’s start with the smallest part of the requirement: your program will return an object. We’ll set up our program file and our test file and then write a simple test to assert that the program to return an object.

Let’s start with friend-contact.js in the root directory:

function friendContact(obj) {
}
module.exports = friendContact;

Then, in [rootDir]/test, create a file named friend-contact.spc.js:

const assert = require('assert');
const friendContact = require('../friend-contact');

describe('friend contact', function () {
it('returns an object', function () {
assert.equal('object', typeof friendContact());
});
});

Now, if you have mocha installed globally (npm install mocha –g), you should be able to run mocha from your root directory and it will pick up your test in the test directory. Because friendContact is an empty function right now, it should fail:

First test failing

Let’s go ahead and get this test to pass. Remember, we want to wright the minimum amount of code possible to get the test to pass, so we’ll just hard code friendContact to return an object:

function friendContact(obj) {
return {}
}
module.exports = friendContact;

Now, run the test, and it should pass:

First test passing

Now, this might seem like cheating to hard code an expected return like this. We’re not going to leave it this way and it’s important to note the benefit of doing the testing and development this way. If your tests ever start failing you won’t get much information if your one bigTestThatCoversEverythingFoo fails. However, if you’ve got many tests that cover progressive requirements of your module, you’ll have an idea of where to start debugging if something goes wrong.

At this point, it’s usual in TDD to refactor the code to make it read or look or work better, but this is really straight ahead, so we won’t do that part here.

Now, let’s write our next test. We’ve already talked about error handling with input, so let’s make sure that our input is what we expect it to be. First, we know that it should take an object:

it('will not throw when passed an object', function () {
assert.doesNotThrow(friendContact.bind(null, {}));
// see my last post
// (http://jsunittesting.com/2017/10/23/writing-unit-tests-to-handle-unexpected-inputs/)
// about why you should use bind
});

Here, the test should pass, but let’s add error handling tests that won’t pass yet:

describe('bad input error handling', function() {
it('will throw when passed a boolean', function() {
assert.throws(friendContact.bind(null, false), /only object input/, 'wrong error message');
});
it('will throw when passed null', function() {
assert.throws(friendContact.bind(null, null), /only object input/, 'wrong error message');
});
it('will throw when passed undefined', function() {
assert.throws(friendContact.bind(null, undefined), /only object input/, 'wrong error message');
});
it('will throw when passed a number', function() {
assert.throws(friendContact.bind(null, 12), /only object input/, 'wrong error message');
});
it('will throw when passed a string', function() {
assert.throws(friendContact.bind(null, 'foo'), /only object input/, 'wrong error message');
});
it('will throw when passed a symbol', function() {
assert.throws(friendContact.bind(null, Symbol()), /only object input/, 'wrong error message');
});
});

Now run your tests and they should fail. Let’s handle the cases. Remember we’re looking for the quickest way to get our tests to pass. However, if we make the function always throw, our test that asserts that it won’t throw when passed an object will fail. It’s simple to check our input:

function friendContact(obj) {
if (typeof obj !== 'object' || obj === null ) throw new Error ('only object input');
// in JavaScript typeof null === ‘object’, so we need to check that case separately with the OR
return {};
}

Now, if you run the tests, you’ll see the new input handling tests passing. However, you’ll get a new failure: because we now expect friendContact to be passed an object, our first test that passed no argument will now throw, making the test expecting the return of an object fail: (assert.equal('object', typeof friendContact());). Let’s fix it: assert.equal('object', typeof friendContact({}));

Our tests should now all pass. Let’s start actually making our features. First, we need a way to pass test data into our tests. Let’s create a folder called test-data inside the test directory and add a file called friend-data.js and add some test data:

const friendData = {
firstName: 'Clive',
lastName: 'Lewis',
phone: '44 01223 332129',
lastContact: '2017-11-29',
};
module.exports = friendData;

Now, let’s require it in our test file and use it to write a test:

const friendData = require('./test-data/friend-data');
//snip, add next test after the doesNotThrowtest
describe('returned output', function() {
it('returns an object', function () { // moved down from the top of the test
assert.equal('object', typeof friendContact({}));
});
it('returns an object with the expected data', function () {
assert.deepStrictEqual(friendData, friendContact(friendData));
});
});

Run your test, it should fail. Let’s make the test pass:

function friendContact(obj) {
if (typeof obj !== 'object' || obj === null ) throw new Error ('only object input');
return obj;
}
module.exports = friendContact;

Okay, now let’s start doing the work of making this do the lifting that we set out to do in the first place: returning one friend’s data that is the one that you contacted the longest ago. Let’s first build out friendData with more friends. We’ll have an array of objects, each with a firstName, lastName, phone and lastConatact. Build it out with at least three friends and also export an object with the data of the friend you contacted longest ago:

const friendData = [
{
firstName: 'Clive',
lastName: 'Lewis',
phone: '44 01223 332129',
lastContact: '2016-11-29',
},
{
firstName: 'Martin',
lastName: 'Chemnitz',
phone: '49 345 5520',
lastContact: '2016-11-09',
},
{
firstName: 'Dieterich',
lastName: 'Buxtehude',
phone: '49 451 397700',
lastContact: '2017-05-09',
}
];

const solution = {
firstName: 'Martin',
lastName: 'Chemnitz',
phone: '49 345 5520',
lastContact: '2016-11-09',
}

module.exports = {
friendData,
solution
};

Now that we’ve restructured the data, we’ll want to run our tests to make sure that what we’ve got still passes and we don’t need to re-write anything. (Even though it passes now, you will want to change all of the friendData to friendData.friendData to reflect what’s going on.) Surprisingly, they still pass. Now let’s re-write our returned object test asserting that we’ll get the solution if we give the function our new friendData:

it('returns an object with the expected data', function () {
assert.deepStrictEqual(friendData.solution, friendContact(friendData.friendData));
});

As we expect, our test fails. Now, let’s build our function to behave the way we want. Because we’re now expecting arrays, I re-wrote the error handler to only take arrays which required a re-write of some of our tests:

function friendContact(inputArray) {
if (!Array.isArray(inputArray)) throw new Error ('only array input');
let friendIndex;
let lastContact;
inputArray.forEach((obj, index) => {
const currentObjDate = new Date(obj.lastContact);
if (!lastContact) lastContact = currentObjDate;
if (currentObjDate <= lastContact) friendIndex = index;
});
return inputArray[friendIndex] || {};
}

module.exports = friendContact;

Our test suite ended up like this:

const assert = require('assert');
const friendContact = require('../friend-contact');
const friendData = require('./test-data/friend-data');

describe('friend contact', function () {
it('will not throw when passed an array', function () {
assert.doesNotThrow(friendContact.bind(null, []));
});
describe('returned output', function() {
it('returns an object', function () {
assert.equal('object', typeof friendContact([]));
});
it('returns an object with the expected data', function () {
assert.deepStrictEqual(friendData.solution, friendContact(friendData.friendData));
});
});
describe('bad input error handling', function() {
it('will throw when passed a boolean', function() {
assert.throws(friendContact.bind(null, false), /only array input/, 'wrong error message');
});
it('will throw when passed null', function() {
assert.throws(friendContact.bind(null, null), /only array input/, 'wrong error message');
});
it('will throw when passed undefined', function() {
assert.throws(friendContact.bind(null, undefined), /only array input/, 'wrong error message');
});
it('will throw when passed a number', function() {
assert.throws(friendContact.bind(null, 12), /only array input/, 'wrong error message');
});
it('will throw when passed a string', function() {
assert.throws(friendContact.bind(null, 'foo'), /only array input/, 'wrong error message');
});
it('will throw when passed a non-array object', function() {
assert.throws(friendContact.bind(null, {}), /only array input/, 'wrong error message');
});
it('will throw when passed a symbol', function() {
assert.throws(friendContact.bind(null, Symbol()), /only array input/, 'wrong error message');
});
});
});

That’s a good place to stop. Try extending the app some more. Continue to start with tests that fail and write code to make them pass. Here are a few ideas for how you can extend the app:

  • Verify that the friend object has the necessary data and throw if it doesn’t.
  • Decide how to and deal with situations where two friends were both contacted the longest ago on the same day.
  • Add additional contact methods (maybe email, fax), a preferred contact method and return just the preferred contact method and the name.
  • Add a way to add friends.
  • Add the ability to update when someone was contacted last.

That’s TDD. Let me know if you have questions!