Node.js version 20 brings a stable test runner so you can run tests inside *.test.js
files with node --test
command. This post covers the primary usage of it regarding spies and mocking for the unit tests.
Spies are functions that let you spy on the behavior of functions called indirectly by some other code while mocking injects test values into the code during the tests.
mock.method
can create spies and mock async, rejected async, sync, chained methods, and external and built-in modules.
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
const calculationService = {
calculate: () =>
};
describe('mocking resolved value', () => {
it('should resolve mocked value', async () => {
const value = 2;
mock.method(calculationService, 'calculate', async () => value);
const result = await calculationService.calculate();
assert.equal(result, value);
});
});
const error = new Error('some error message');
mock.method(calculationService, 'calculate', async () => Promise.reject(error));
await assert.rejects(async () => calculateSomething(calculationService), error);
mock.method(calculationService, 'calculate', () => value);
mock.method(calculationService, 'get', () => calculationService);
mock.method(calculationService, 'calculate', async () => value);
const result = await calculationService.get().calculate();
import axios from 'axios';
mock.method(axios, 'get', async () => ({ data: value }));
import fs from 'fs/promises';
mock.method(fs, 'readFile', async () => fileContent);
- Async and sync functions called multiple times can be mocked with different values using
context.mock.fn
and mockedFunction.mock.mockImplementationOnce
.
describe('mocking same method multiple times with different values', () => {
it('should resolve mocked values', async (context) => {
const firstValue = 2;
const secondValue = 3;
const calculateMock = context.mock.fn(calculationService.calculate);
calculateMock.mock.mockImplementationOnce(async () => firstValue, 0);
calculateMock.mock.mockImplementationOnce(async () => secondValue, 1);
const firstResult = await calculateMock();
const secondResult = await calculateMock();
assert.equal(firstResult, firstValue);
assert.equal(secondResult, secondValue);
});
});
- To assert called arguments for a spy, use
mockedFunction.mock.calls[0]
value.
mock.method(calculationService, 'calculate');
await calculateSomething(calculationService, firstValue, secondValue);
const call = calculationService.calculate.mock.calls[0];
assert.deepEqual(call.arguments, [firstValue, secondValue]);
- To assert skipped call for a spy, use
mockedFunction.mock.calls.length
value.
mock.method(calculationService, 'calculate');
assert.equal(calculationService.calculate.mock.calls.length, 0);
- To assert how many times mocked function is called, use
mockedFunction.mock.calls.length
value.
mock.method(calculationService, 'calculate');
calculationService.calculate(3);
calculationService.calculate(2);
assert.equal(calculationService.calculate.mock.calls.length, 2);
- To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using
mockedFunction.mock.calls[index]
and call.arguments
values.
const calculateMock = context.mock.fn(calculationService.calculate);
calculateMock.mock.mockImplementationOnce((a) => a + 2, 0);
calculateMock.mock.mockImplementationOnce((a) => a + 3, 1);
calculateMock(firstValue);
calculateMock(secondValue);
[firstValue, secondValue].forEach((argument, index) => {
const call = calculateMock.mock.calls[index];
assert.deepEqual(call.arguments, [argument]);
});
Running TypeScript tests
Add a new test script, --experimental-transform-types
flag requires Node version >= 22.10.0
{
"type": "module",
"scripts": {
"test": "node --test",
"test:ts": "NODE_OPTIONS='--experimental-transform-types --disable-warning=ExperimentalWarning' node --test ./src/**/*.{spec,test}.ts"
}
}
Demo
The demo with the mentioned examples is available here.