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:
- Create a separate file from your
test-foo.spec.js
(or whatever your test file is) and call it something liketest-foo-data.js
. (I like to create a separate folder inside my test folder calledtest-data
and put my data in there.) -
In your test file, create your data, assign it to a
const
and export eachconst
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.