Introduction
Jest makes unit testing painless and easier compared to using other frameworks such as using Mocha
, chai
, and Sinon
.
Let’s dive right in!
What jest offers
Jest acts both as a test runner(runs your tests) and acts as a test suite(providing assertions using matchers). With Jest, everything becomes easy to set up as it provides an all-in-one solution for carrying out unit testing.
All code for all examples in this article can be found in this unit testing with jest and is all open for any use.
1. Setting up jest
- Create a new project
mkdir jest-unit-testing
cd jest-unit-testing
npm init -y
- Add
jest
as a devDependency to the project
npm install --save-dev jest
-
Add
jest
globally to be able to use later to generatejest.config.js
files -
Generate a
jest.config.js
file for controlling options for running your tests usingjest
such ascollectCoverage
among other powerful options to choose from
npm install -g jest
# inside the project root
jest --init
2. Our first test case
To confirm that jest
has been set up correctly and working let’s write a dummy test case, but first..
Globals
Jest offers globals- describe()
and expect
so one does not have to explicitly import these inside their *.test.js
files. It’s possible but not necessary.
It’s also a convention by jest to name files with a .test.js
or .test.ts
if using TypeScript to separate source files from test files, jest
runs any file with this naming convention but can also be overridden in the config file.
- create a
src/
folder inside the project, create anindex.js
and add the following lines:
const sum = (a, b) => a + b
module.exports = sum
- Create
index.test.js
and add the following lines:
describe('addition', () => {
test('adds 2 numbers and returns their sum', () => {
expect(sum(2,2)).toBe(4)
})
})
Explanation
describe
wraps all our testcases
(test
) under one descriptive name addition
which is called a test suite
.
expect(expression)
is the assertion portion and .toBe(expected)
is the matcher.
Jest offers a lot of matchers to match different assertions.
Here are a few common matchers that jest
offers:
const square = (a) => a * a
test('square of 5 is 25', () => {
expect(square(5)).toBe(25)
})
describe('Test Matchers', () => {
test('null', () => {
const n = null
expect(n).toBeNull()
expect(n).toBeDefined()
expect(n).not.toBeUndefined()
expect(n).not.toBeTruthy()
expect(n).toBeFalsy()
})
test('zero', () => {
const z = 0
expect(z).not.toBeNull()
expect(z).not.toBeUndefined()
expect(z).not.toBeTruthy()
expect(z).toBeFalsy()
})
test('adding floating point numbers', () => {
const value = 0.1 + 0.2
console.log(value)
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3) // This works.
})
test('should test arrays', () => {
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk'
]
expect(shoppingList).toContain('milk')
})
test('should test strings', () => {
expect('Naftali').toMatch(/Naf/)
})
test('should test exceptions', () => {
const compileAndroidCode = () => {
throw new Error('JDK not found in path')
}
expect(() => compileAndroidCode()).toThrow(Error)
})
})
Each matcher should narrow down and be close to the expected behavior as much as possible.
Testing async behavior
Testing promises and async behavior is quite straightforward. Here are a few gotchas that will save you a lot of head-scratching:
- Always wrap your asynchronous calls with
try...catch
if usingasync/await
to avoidunhandled promise
rejection. - Ensure that promises resolve and be sure to always append the
.catch(err=>{})
when consuming a Promise using the.then
syntax.
Doing the above solves one hidden problem that I noticed: In jest if asynchronous code doe not resolves or runs into the common unhandled promise rejection
error, the tests timeout. The error looks like this: Timeout of 5000ms exceeded
causing the test to fail. This has been asked a lot on Stackoverflow.
Test async behavior
Assuming we are fetching data from a web API over HTTP. We’d like to test that whatever the API returns are exactly what we expected(We won’t use mocking because, if the API changes the tests would still work).
- To test
asynchronous
behavior we prependasync
keyword to the callback passed totest()
. The test suite looks more like:
const fetchTodos = async () => {
try {
const res = await fetch('httpss://jsonplaceholder.typicode.com/todos/1')
const todos = await res.json() // parse response back to plain Js objects
return todos
} catch(e) {
console.error(`fetching failed with`, e)
}
}
The tests case:
import fetchTodos from './fetchTodos'
describe('test async', () => {
test('returns API response todos', async() => {
const todos = await fetchTodos()
expect(todos).toBeArray()
})
})
Testing async behavior becomes as trivial as above. One thing, go back to fetchTodos.js
and remove the try...catch
block, run the test and see what happens to the test results.
Extending the matchers with community provided matches
Jest offers an option to extend the matchers using custom matchers. However, there are custom matchers provided by the lovely community packaged as jest-extended
npm module.
- To extend
jest
matchers, addjest-extended
as a devDependency,
npm install --save-dev jest-extended
- Update
jest.config.js
to enable extending in all/single testfiles.
jest.config.js
const config = {
testEnvironment: 'node',
verbose: true,
automock: false,
clearMocks: true,
collectCoverage: true,
// https://github.com/jest-community/jest-extended: Extended matchers
setupFilesAfterEnv: ['jest-extended/all'],
}
exports = config
- Extend matchers inside your testfiles:
const matchers = require('jest-extended')
expect.extend(matchers)
jest-extended
provides a lot of matchers that are useful especially if default matchers do not cover the behavior to be asserted.
Read more about these matchers here jest-extended-matchers
Summary
Jest makes testing a breeze, It was made to be as simple as it is. jest
can be used as a test runner with a separate library like chai
to act as the test suite. react-testing-library
which is used as an alongside jest
as a test suite for unit testing React
applications
A few notes:
- Always use the matcher that is close to the behavior being tested eg.
expect(true).toBe(true)
instead ofexpect(true).toBeTruthy()
the former is closer to the behavior being tested in the case compared to the latter though both would still pass.
NB: Add this to "types:
tsconfig.config.ts"
to enable the Jest TypeScript types:
npm i -D @types/jest
{
"types": ["jest"] // enables `jest intellisense`
}
The above entry resolves this type warning:
- Always ensure
Promises
and anyasynchronous
Promise rejections are handled else, you’ll get aTimeout of 5000ms exceeded
error fromjest
- Always feel free to use extended matchers if current default matchers do not match the behavior being tested in your code.
Read more about Jest here and extended matchers here using matchers