Material available on GitHub.
You can create a simple assertion library by creating a function that returns an object that holds a method. This lets us use the expect(result).toBe(expectedResult)
structure. However, this solution will only run the first item, and it also hides the exact location of any errors that pop up.
let result = sum(3, 7)
let expected = 10
expect(result).toBe(expected);
result = subtract(7, 3)
expected = 10
expect(result).toBe(expected);
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) {
throw new Error(`Expected result to be ${expected}, got ${actual} instead.`)
}
}
}
}
We can improve upon our little assertion library by adding a simple framework around it. The framework, or a test
function, wraps the tests it runs inside a try-catch block to prevent errors from stopping the program. The test function accepts two arguments, a title and a callback function. The title is the name of the test that's being run and the callback function contains the setup of the test and a call to the expect
function:
function test(title, callback) {
try {
callback()
console.log(`✔️ ${title}`)
} catch (error) {
console.error(`❌ ${title}`)
console.error(error)
}
}
test('subtract subtracts numbers', () => {
const result = subtract(7, 3)
const expected = 4
expect(result).toBe(expected);
})
This lets the users know which tests produced errors and which ones ran successfully, as well as providing a stack trace that provides more details.
Jest lets you use several types of assertions when running tests:
The toBe
assertion is similar to ===
. You can use it to check if primitive values match, or check if two variables refer to the same object. Note that even if two objects that have been created separately have the same property values, they will not match with toBe
as they are two different objects.
test('toBe', () => {
expect(1).toBe(1)
expect(true).toBe(true)
expect({}).not.toBe({})
])
If you need to compare objects (or arrays), you can use the toEqual
assertion. This lets you match two separate objects that are "visually" identical.
test('toEqual', () => {
const subject = { a: { b: 'c' }, d: 'e' }
const actual = { a: { b: 'c' }, d: 'e' }
expect(subject).toEqual(actual)
const subArray = [1, 2, { three: 'four', five: { six: 7 }}]
const actArray = [1, 2, { three: 'four', five: { six: 7 }}]
expect(subArray).toEqual(actArray)
})
In case you need to check for partial matches with objects or arrays, you can use the toMatchObject
assertion. This assertion passes if the subject contains at least the properties that are defined in the object/array it is being compared against.
test('toMatchObject', () => {
const subject = { a: { b: 'c' }, d: 'e' }
const actual = { a: { b: 'c' }}
expect(subject).toMatchObject(actual)
const subArray = [1, 2, { three: 'four', five: { six: 7 }}]
const actArray = [1, 2, five: { six: 7 }}]
expect(subArray).toMatchObject(actual)
})
When comparing objects, Jest lets you use schemas to check an object's structure in case you only want to check that, and not the actual property values:
test('using schemas', () => {
const birthday = {
day: 15,
month: 04,
year: 1984,
meta: { display: 'Apr 15th, 1984' }
}
const schema = {
day: expect.any(Number),
month: expect.any(Number),
year: expect.any(Number),
meta: { display: expect.stringContaining('1984') }
// there's also expect.arrayContaining() and expect.objectContaining()
}
expect(birthday).toEqual(schema)
})
You can use Jest to create mock functions to be used in tests. The mock function object contains plenty of data about how the mock function was used during the tests:
test('mock functions', () => {
const myFn = jest.fn()
myFn('first', { second: 'value' })
const allCalls = myFn.mock.calls
const firstCall = allCalls[0]
const firstArg = firstCall[0]
const secondArg = firstCall[1]
expect(firstArg).toBe('first')
expect(secondArg).toEqual({ second: 'value' })
})
Unit tests typically target small units of code, for example functions. Unit tests make sure that the function is behaving as expected and, for instance, returns correct, expected values.
import { isPasswordAllowed } from '../auth'
test('isPasswordAllowed rejects non-allowed passwords', () => {
// Make sure all assertions get run. Useful especially with async testing
expect.assertions(4)
expect(isPasswordAllowed('')).toBe(false)
expect(isPasswordAllowed('ffffffff')).toBe(false)
expect(isPasswordAllowed('88888888')).toBe(false)
expect(isPasswordAllowed('Nakkimaakari-77')).toBe(true)
})
In addition to using functionality that Jest provides, like the expect.assertions()
, it is good practice to make sure the tests work and are run by breaking both the test AND source code when writing the tests.
The example above contains multiple similar assertions. To enhance readability, we can group these tests under a describe()
block. We can also make the results more descriptive and the test capable of handling multiple inputs by creating a test factory using arrays and forEach
.
describe('isPasswordAllowed', () => {
const allowedPasswords = ['Nakkimaakari-77]
const disallowedPasswords = ['', 'ffffffff', '88888888']
allowedPasswords.forEach(pwd => {
it(`"${pwd}" should be allowed`, () => {
expect(isPasswordAllowed(pwd)).toBe(true)
})
}
disallowedPasswords.forEach(pwd => {
it(`"${pwd}" should not be allowed`, () => {
expect(isPasswordAllowed(pwd)).toBe(false)
})
}
})
Most commonly used tool for managing tests' code coverage is Istanbul. To use it with Jest, simply add --coverage
parameter to the Jest command in package.json.
Monkey patching is a quick and dirty way to create, for instance, mock functions. This involves importing (if the function is in a separate file) the function that needs to be mocked, storing the original function in a variable, and replacing it with a mocked version for the duration of the test. At the end of the test, you should revert back to the original function in order to not break other tests. Monkey patching is not a recommended practice, though, as it breaks the contract between the original code and the tests. For example, if someone were to update the getWinner
function in the utils file by adding an extra parameter, the test below would still pass, but would have no connection to the updated getWinner
function.
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const originalGetWinner = utils.getWinner;
utils.getWinner = (p1, p2) => p2;
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
utils.getWinner = originalGetWinner
})
To make sure that changing the number of parameters in a function does not break the link between the test and the actual code, you could do some assertions regarding the mocked function and keep tabs on how many arguments are passed into the function. Below, we add a mock object as a property to the getWinner
function. Every time the function is called, we push the arguments passed to the function into the mock.calls
array, so that we can check how many times the function was called, and to check what arguments were passed to the function. We also return the value from the args
array. This way, if the position/number of the parameters changes in the original function, we'll notice that when the test breaks.
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const originalGetWinner = utils.getWinner
utils.getWinner = (...args) => {
utils.getWinner.mock.calls.push(args)
return args[1]
}
utils.getWinner.mock = { calls: [] }
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach(element => {
expect(element).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
});
utils.getWinner = originalGetWinner
})
The jest.spyOn
method lets you keep tabs on calls to a certain object and calls to a method within that object. In addition, you can use the mockImplementation()
method to create a mock function to replace the original functionality. Using the jest.spyOn
method, mocking the original function (and restoring it after the test) becomes quite a bit cleaner:
import thumbWar from '../thumb-war'
import * as utils from '../utils'
test('returns winner', () => {
const spy = jest.spyOn(utils, 'getWinner').mockImplementation((p1, p2) => p2)
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
spy.mockRestore()
})
Note that even with jest.spyOn
, we are still modifying the utils
import namespace. This time around, Jest is the one doing the modifications. But since this is not exactly safe, we'll need another way, still, to handle mocking. This is where jest.mock
comes in handy.
The jest.mock
method lets you define a module that you want to mock as its first argument, and define a function that returns a module with which you want to replace the original module as the second argument. And you can really think of the return value of the function as a completely new module that you would start building in a new file with the exception that instead of creating a module.exports
or export default ...
you are simply returning the items. This is made possible by the fact that Jest takes over the module system from Node, and every import statement goes through Jest first, letting Jest see which imports should be replaced with a mock. Any mocks you create in a test file only applies to the tests in that specific file.
import thumbWar from '../thumb-war'
import * as utils from '../utils'
jest.mock(
'../utils',
() => {
return {
getWinner: jest.fn((p1, p2) => p2)
}
}
)
test('returns winner', () => {
const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
expect(winner).toBe('Kent C. Dodds')
expect(utils.getWinner).toHaveBeenCalledTimes(2)
utils.getWinner.mock.calls.forEach(args => {
expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})
})
When you create a mock version of a module, you don't have to mock each and every property and method in the module. Instead, you can easily use the original versions from the module for any items you don't need a mock for and just mock the ones you need. To do this, you use Jest's require.requireAll()
method to require/import the original module, use that as the basic building block for the mock, and simply override the properties/methods that you need to.
import * as utils from '../utils'
jest.mock(
'../utils',
() => {
const actualUtils = require.requireActual('../utils')
return {
...actualUtils,
getWinner: jest.fn((p1, p2) => p2)
}
}
)
If we were to run the test for getting the winner twice after mocking the getWinner()
method, we would get an error in the second run, since the test checks how many times the method has been run. To avoid this, we can clear or reset the mock before every run:
beforeEach(() => {
utils.getWinner.mockClear()
})
In addition to mocking out modules individually in files, Jest lets you create mocks that you can define once and use in multiple files. To do this, create a __mocks__
folder next to the __tests__
folder, and create a file with the same name as the module you want to mock inside it, and in the file, create the mock implementation of the file
Utils
|-- __mocks__
| |-- utils.js
|
|-- __tests__
| |--
| |--
|
|-- utils.js
For any test files that want to use this mocked solution, run the jest.mock
method without adding an implementation as a second parameter, and Jest will go look for the mocked implementation from the __mocks__
folder.
import * as utils from '../utils'
jest.mock('../utils')
When testing for instance Express applications, we need to create test objects like req
and res
to be used in the tests. These objects are often either identical, or very similar to each other. Instead of creating these objects again and again in each test, we can create a setup function that returns common versions of both objects that can be used in tests:
function setup() {
const req = {
body: {}
}
const res = {}
Object.assign(res, {
status: jest.fn(
function status() {
return this;
}.bind(res),
),
json: jest.fn(
function json() {
return this;
}.bind(res),
),
send: jest.fn(
function send() {
return this;
}.bind(res),
),
})
return { req, res }
}
In our tests, we can the use the setup function to create our req
and res
objects, and customize them as needed:
test("it returns some stuff of other", () => {
const { req, res } = setup();
req.params = { id: "testUserId"}
...
})
A common work pattern for test driven development is the red-green-refactor pattern. First, you write as little code as possible to get a failing test, then write as little code as possible to get the test passing, and then refactor both the code and the test into something that resembles shippable code.