Spies and mocking with Node.js test runner (node:test)
Tags: javascript, typescript, node, testing
Published: 2023-06-24 21:08:24

---
title: Spies and mocking with Node.js test runner (node:test)
published: true
tags: ['javascript', 'typescript', 'node', 'testing']
canonical_url: https://sevic.dev/notes/spies-mocking-node-test-runner/
---

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.

- Async function

```js
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';

const calculationService = {
  calculate: () => // implementation
};

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);
  });
});
```

- Rejected async function

```js
const error = new Error('some error message');
mock.method(calculationService, 'calculate', async () => Promise.reject(error));

await assert.rejects(async () => calculateSomething(calculationService), error);
```

- Sync function

```js
mock.method(calculationService, 'calculate', () => value);
```

- Chained methods

```js
mock.method(calculationService, 'get', () => calculationService);
mock.method(calculationService, 'calculate', async () => value);

const result = await calculationService.get().calculate();
```

- External modules

```js
import axios from 'axios';

mock.method(axios, 'get', async () => ({ data: value }));
```

- Built-in modules

```js
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`.

```js
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.

```js
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.

```js
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.

```js
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.

```js
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

```json
{
  "type": "module",
  "scripts": {
    "test": "node --test",
    "test:ts": "glob -c \"node --loader tsx --no-warnings --test\" \"./src/**/*.{spec,test}.ts\""
  },
  "devDependencies": {
    // ...
    "glob": "^10.3.1",
    "tsx": "^3.12.7"
  }
}
```

### Demo

The demo with the mentioned examples is available [here](https://sevic.dev/demos).