This site lists the protips that we shared with students during our courses
When you get into writing unit tests you soon will run into dependencies that your code has to external functionality and resources. It can be file-I/O, network calls, fetching from external API, or Dates.
A unit test is not a unit test if it uses these external dependencies - that would rather be an integration test. The difference between these two is more important than you first may think. Here are three things that pop into my head:
new Date()
is not your concern.fetch
to an external resource then that resource needs to be up and working, for us to verify our code. That is not really their concern.The solution to many of these problems is to mock or fake the dependency. Mocking is a minefield of terms but here I’m going to take a pragmatic approach and let it mean something that we use in our test instead of the real thing - I think that is a test fake.
Luckily the JavaScript language helps us here, as we can change and replace functions on the fly thanks to the dynamic nature of the language.
There are many libraries that will help you to set up mocking (or spying or faking or …), but we are going to do it by hand - because it’s not that hard and often quite enough.
We’re going to use two examples; dates and calling an external service with fetch
Quite a lot of our code depends on dates. It can be a simple little thing like calculating someone’s current age, or days from a certain date until today, etc. The keyword here is that we want to know what the current date is to calculate these values. It might look like this:
module.exports.caluclateAge = birthYear => new Date().getFullYear() - birthYear;
console.log(caluclateAge('1972')); // prints 48
The problem is of course that if I write a test for this function - it will only work … this year:
describe('Age calulcator', () => {
const ageCalulator = require('./ageCalculator');
it('born 1972 = 48', () => {
const age = ageCalulator.caluclateAge(1972);
assert.strictEqual(age, 48);
});
});
In this case, the fix is pretty easy… Let’s pass in the current year instead:
module.exports.caluclateAge = (year, birthYear) => year - birthYear;
// test
describe('Age calulcator', () => {
const ageCalulator = require('./ageCalculator');
it('born 1972 at 2020 = 48', () => {
const age = ageCalulator.caluclateAge(2020, 1972);
assert.strictEqual(age, 48);
});
});
But at some point, some code will need to pass the date in… we basically just moved the problem one step up. Let’s fix it with mocking instead.
We can do that by introducing a little function to get the current year and then use it in the calculateAge
:
module.exports.getYear = () => new Date().getFullYear();
module.exports.caluclateAge = birthYear => this.getYear() - birthYear;
console.log(this.caluclateAge(1972));
When we call this from the test we can now, before we call the test, add another version of the getYear
-function. One that always returns the same thing so that we know how it behaves:
const assert = require('assert');
const ageCalulator = require('.');
describe('Age calulcator', () => {
it('born 1972 at 2012 = 40 years old', () => {
ageCalulator.getYear = () => 2012;
const age = ageCalulator.caluclateAge(1972);
assert.strictEqual(age, 40);
});
});
In the case of the test we will now, instead of calling module.exports.getYear = () => new Date().getFullYear()
call our replacement, mock version ageCalulator.getYear = () => 2012;
. As you can see that version of getYear
always returns the same thing.
If you have many case, you might want to set the mocking up in a beforeEach
-block:
const assert = require('assert');
const ageCalulator = require('.');
describe('Age calulcator', () => {
beforeEach(() => {
ageCalulator.getYear = () => 2012;
});
it('born 1972 at 2012 = 40 years old', () => {
const age = ageCalulator.caluclateAge(1972);
assert.strictEqual(age, 40);
});
it('born 1977 at 2012 = 35 years old', () => {
const age = ageCalulator.caluclateAge(1977);
assert.strictEqual(age, 35);
});
it('born 1972 at 2012 = 4 years old', () => {
const age = ageCalulator.caluclateAge(2008);
assert.strictEqual(age, 4);
});
});
This is especially useful if the mock-version is a bit more complicated, as it might be in the case of calling an external service.
Here’s another code snippet that is not uncommon; calling an external service with fetch
const fetch = require('node-fetch');
module.exports.iNeedAHero = id =>
fetch(`https://swapi.dev/api/people/${id}`)
.then(res => res.json())
.then(data => `Here's a true hero ... ${data.name}`);
this.iNeedAHero(14).then(console.log);
Ok - very simple here, but still a problem. This is slow, swap.dev status is not affecting my unit. I cannot test this without testing both fetch
and the SwAPI…
Also, I’m not particularly fond of how the getting data and converting it to json is blended with my “business logic” (in this case represented by creating this string This is ${data.name}, he is ${data.height} tall
).
Let’s do the same trick and break out a little helper function:
const fetch = require('node-fetch');
module.exports.fetchJSON = url => fetch(url).then(res => res.json());
module.exports.iNeedAHero = id => {
const url = `https://swapi.dev/api/people/${id}`;
return this.fetchJSON(url).then(data => `This is ${data.name}, he is ${data.height} tall`);
};
this.iNeedAHero(14).then(console.log);
That works the same and is a bit clearer if you ask me. But more importantly, we can now fake this in a test:
describe('Hero getter', () => {
beforeEach(() => {
sut.fetchJSON = () =>
new Promise((resolve, _) =>
resolve({
name: 'Marcus Solo',
height: '195',
hair_color: 'brown',
})
);
});
it('Fetches a REAL hero', () =>
sut
.iNeedAHero(2345251251)
.then(str =>
assert.strictEqual(str, `This is Marcus Solo, he is 195 tall`)
));
});
Ok - quite a lot of code there, but nothing new:
beforeEach
we set up a mock version of the fetchJSON
-function
fetch
returns a promise so that we mimic the behavior.resolve
the promise since this will return a hard code hero (not that the hero in question is particularly impressive :))reject
the promise, making it fail and test our (for now) non-existent error handling codedone
variable passed in and instead we are returning the promise out of the it
-function. If it fulfills mocha will indicate it as passing. Should the promise rejects mocha indicates it as failing..then
-blockBut this means that we can now test all the code that is not fetching or doing networking separately from the code that does our business logic.
There are tools to help us do mocking and what I’ve described above can be considered a poor mans version. But it is actually enough for many cases.
A few things that you might miss are:
Mocking is a powerful tool to ensure that we test the unit separately from its dependency. It’s not hard to set up and often leads to better-written code, where different types of functionalities are separated from each other.
(PS there’s a case against mocking as well … which I leave up to the reader to explore and make up your mind about. I’m torn between both)
The code for this post is found here