With apps being as complex as they are today, we need some way to verify that our changes to apps didn’t cause regressions. To do this, we need unit tests.
We can add unit tests to JavaScript apps easily with the Jest test runner. It’s very easy to get running and we can write lots of tests with it that runs quickly.
In this article, we’ll look at how to set up Jest from scratch and write some tests with it. Both synchronous an asynchronous functions will be tested.
Getting Started
To get started, we simply run a few commands. In our app’s project folder, we run:
yarn add --dev jest
or
npm install --save-dev jest
to add Jest to our JavaScript apps.
The example we have below will have 2 simple scripts each containing a module that has several functions.
First we create the code that we’re testing. We’ll test both code that does and doesn’t call APIs.
We create example.js
and put in the following code:
const add = (a, b) => a + b;
const identity = a => a;
const deepCopy = (obj, copiedObj) => {
if (!copiedObj) {
copiedObj = {};
}
for (let prop of Object.keys(obj)) {
copiedObj = {
...copiedObj,
...obj
};
if (typeof obj[prop] === 'object' && !copiedObj[prop]) {
copiedObj = {
...copiedObj,
...obj[prop]
};
deepCopy(obj[prop], copiedObj);
}
}
return copiedObj;
}
module.exports = {
add,
identity,
deepCopy
}
The code above has functions to add numbers and a function that returns the same thing that it passes in.
Then we create request.js
with the following:
const fetchJokes = async () => {
const response = await fetch('[http://api.icndb.com/jokes/random/'](http://api.icndb.com/jokes/random/%27));
return response.json();
};
module.exports = {
fetchJokes
}
The code above gets random jokes from the Chuck Norris Jokes API.
Writing Tests for Synchronous Code
Now that we have the code that we want to test, we’re ready to create some test code for them.
First we create tests for the functions in example.js
.
We add the following tests:
const { add, identity } = require('./example');
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('adds 1 + 2 is truthy', () => {
const sum = add(1, 2);
expect(sum).toBeTruthy();
});
test('adds 1 + 2 to be defined', () => {
const sum = add(1, 2);
expect(sum).not.toBeUndefined();
expect(sum).toBeDefined();
});
test('identity(null) to be falsy', () => {
expect(identity(null)).toBeFalsy();
expect(identity(null)).not.toBeTruthy();
});
The code above is very simple. It imports the functions from example.js
and runs tests with it.
The first test calls add
from example.js
by passing in 2 numbers and checking that the returned result is what we expect.
The 2 tests below it are very similar except that we use different matcher functions to check the results.
The last test runs the identity
function with null
and they use the toBeFalsy
and not.toBeTruthy
matchers to check that null
is indeed falsy.
In summary, Jest has the following matchers:
toBeNull
— matches onlynull
toBeUndefined
— matches onlyundefined
toBeDefined
— is the opposite oftoBeUndefined
toBeTruthy
— matches anything truthytoBeFalsy
— matches anything falsytoBeGreaterThan
— check if a value is bigger than what we expecttoBeGreaterThanOrEqual
— check if a value is bigger than or equal to what we expecttoBeLessThan
— check if a value is less than what we expecttoBeLessThanOrEqual
— check if a value is less than or equal what we expecttoBe
— check if a value is the same usingObject.is
to compare the valuestoEqual
— checks every field recursively of 2 objects to see if they’re the sametoBeCloseTo
— check for floating point equality of values.toMatch
— check if a string matches a give regextoContain
— check if an array has the given valuetoThrow
— check if an exception is thrown
The full list of expect
methods are here.
We can use some of them as follows by adding them to example.test.js
:
test('but there is a "foo" in foobar', () => {
expect('foobar').toMatch(/foo/);
});
test(
'deep copies an object and returns an object with the same properties and values',
() => {
const obj = {
foo: {
bar: {
bar: 1
},
a: 2
}
};
expect(deepCopy(obj)).toEqual(obj);
}
);
In the code above, we use the toMatch
matcher to check if 'foobar'
has the 'foo'
substring in the first test.
In the second test, we run our deepCopy
function that we added earlier and test if it actually copies the structure of an object recursively with the toEqual
matcher.
toEqual
is very handy since it checks for deep equality by inspecting everything in the object recursively rather than just for reference equality.
Writing Tests for Asynchronous and HTTP Request Code
Writing tests for HTTP tests requires some thinking. We want the tests to run anywhere with or without an internet connection. Also, the test shouldn’t depend on the results of the live server since it’s supposed to be self-contained.
This means that we have to mock the HTTP request rather than calling our code directly.
To test our fetchJokes
function in request.js
, we have to mock the function.
To do this, we create a __mocks__
folder in our project folder and in it, create requests.js
. The file name should always match the filename with the code that we’re mocking.
We can mock it as follows:
const fetchJokes = async () => {
const mockResponse = {
"type": "success",
"value": {
"id": 407,
"joke": "Chuck Norris originally wrote the first dictionary. The definition for each word is as follows - A swift roundhouse kick to the face.",
"categories": [
]
}
};
return Promise.resolve(mockResponse);
};
module.exports = {
fetchJokes
}
Both the real and mock fetchJokes
function returns a promise. The real function returns a promise from the fetch
function while the mock one calls Promise.resolve
directly.
This means that the real one actually makes the GET request and the mock tests resolve to the response that we want to test for.
Then in example.test.js
, we add:
jest.mock('./request');
const { fetchJokes } = require('./request');
to the top of the file and:
test('jokes to be fetched', async () => {
const mockResponse = {
"type": "success",
"value": {
"id": 407,
"joke": "Chuck Norris originally wrote the first dictionary. The definition for each word is as follows - A swift roundhouse kick to the face.",
"categories": [
]
}
};
await expect(fetchJokes()).resolves.toEqual(mockResponse)
});
to the bottom of it.
The test above just call the mock fetchJokes
function since we have:
jest.mock('./request');
However, we still need to import the real one since the real one is replaced with the mock one when the test runs.
resolves
will resolve the promise returned from the mock fetchJokes
function and toEqual
will check the structure of the response from the promise’s resolved value.
Finally, in the scripts
section of package.json
, we put:
"test": "jest"
so we can run npm test
to run the tests.
Then when we run npm test
, we should get:
PASS ./example.test.js
√ adds 1 + 2 to equal 3 (2ms)
√ adds 1 + 2 is truthy
√ adds 1 + 2 to be defined (1ms)
√ identity(null) to be falsy
√ but there is a "foo" in foobar
√ deep copies an object and returns an object with the same properties and values (1ms)
√ jokes to be fetched (1ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 1.812s, estimated 2s
Ran all test suites.
No matter how and when we run the tests, they should still pass since they’re independent of each other and don’t rely on external network requests.
Conclusion
When we write unit tests, we have to make sure each test is independent from each other and also should make any network requests.
Jest makes testing easy by letting mock things like code that makes HTTP requests easily.
It’s also easy to set up, and has lots of matchers for all kinds of tests.