-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6ca0f24
commit f54568d
Showing
3 changed files
with
276 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
--- | ||
title: Mocking in tests | ||
layout: learn | ||
authors: JakobJingleheimer | ||
--- | ||
|
||
# Mocking in tests | ||
|
||
Tests should be deterministic, runnable in any order any number of times and always produce the same result. Proper setup and mocking make that possible. | ||
|
||
Node.js provides many ways to mock various pieces of code. | ||
|
||
But first, there are several types of tests: | ||
|
||
type | description | example | mock candidates | ||
:-- | :-- | :-- | :-- | ||
unit | the smallest bit of code you can isolate | `const sum = (a, b) => a + b` | own code, external code, external system | ||
component | a unit + dependencies | `const arithmetic = (op = sum, a, b) => ops[op](a, b)` | external code, external system | ||
integration | components fitting together | - | external code, external system | ||
end-to-end (e2e) | app + external data stores, delivery, etc | A fake user (ex a Playwright agent) literally using an app connected to real external systems. | none (do not mock) | ||
|
||
There are varying schools of thought about when you should and should not mock, the broadstrokes of which are laid out below. | ||
|
||
## When and not to mock | ||
|
||
There are 3 main mock candidates: | ||
|
||
* own code | ||
* external code | ||
* external system | ||
|
||
### Own code | ||
|
||
This is what you/your project control. | ||
|
||
```mjs displayName="your-project/main.mjs" | ||
import foo from './foo.mjs'; | ||
|
||
|
||
export function main() { | ||
const f = foo(); | ||
} | ||
``` | ||
|
||
Here, `foo` is an "own code" dependency of `main`. | ||
|
||
#### Why | ||
|
||
For a true unit test of `main`, `foo` should be mocked: you're testing that `main` works, not that `main` + `foo` work (that's a different test). | ||
|
||
#### Why not | ||
|
||
Mocking `foo` can be more trouble than worth, especially when `foo` is simple, well-tested, and rarely updated. | ||
|
||
Others argue that not mocking `foo` is better because it's more authentic and increases coverage of `foo` (because `main`'s tests will also verify `foo`). This can, however, create noise: when `foo` breaks, a bunch of other tests will also break, so tracking down the problem is more tedious: if only the 1 test for the item ultimately responsible for the issue is failing, that's very easy to spot; whereas 100 tests failing creates a needle-in-a-haystack to find the real problem. | ||
|
||
### External code | ||
|
||
This is what you/your project does not control. | ||
|
||
```mjs displayName="your-project/main.mjs" | ||
import bar from 'bar'; | ||
|
||
|
||
export function main() { | ||
const f = bar(); | ||
} | ||
``` | ||
|
||
Here, `bar` is a node_module installed via npm (or yarn, etc). | ||
|
||
Uncontroversially, for unit tests, this should always be mocked. For component and integration tests, whether to mock depends on what it is. | ||
|
||
#### Why | ||
|
||
Verifying that someone else's code works with your code is not the role of a unit test (and their code should already be tested). | ||
|
||
#### Why not | ||
|
||
Sometimes, it's just not realistic to mock. For example, you would almost never mock react (the medicine would be worse than the ailment). | ||
|
||
### External system | ||
|
||
These are things like databases, environments (Chromium or Firefox for a web app, an operating system for a node app, etc), file systems, memory store, etc. | ||
|
||
Ideally, mocking these would not be necessary. Aside from somehow creating isolated copies for each case (usually very impractical, due to cost, additional execution time, etc), the next best option is to mock. Without doing, tests sabotage each other: | ||
|
||
```mjs displayName="storage.mjs" | ||
import { db } from 'db'; | ||
|
||
|
||
export function read(key, all = false) { | ||
validate(key, val); | ||
|
||
if (all) return db.getAll(key); | ||
|
||
return db.getOne(key); | ||
} | ||
|
||
export function save(key, val) { | ||
validate(key, val); | ||
|
||
return db.upsert(key, val); | ||
} | ||
``` | ||
|
||
```mjs displayName="storage.test.mjs" | ||
import assert from 'node:assert/strict'; | ||
import { describe, it } from 'node:test'; | ||
|
||
import { db } from 'db'; | ||
|
||
import { save } from './storage.mjs'; | ||
|
||
|
||
describe('storage', { concurrency: true }, () => { | ||
it('should retrieve the requested item', async () => { | ||
await db.upsert('good', 'item'); // give it something to read | ||
await db.upsert('erroneous', 'item'); // give it a chance to fail | ||
|
||
const results = await read('a', true); | ||
|
||
assert.equal(items.length, 1); // ensure read did not retrieve erroneous item | ||
|
||
assert.deepEqual(results[0], { key: 'good', val: 'item' }); | ||
}); | ||
|
||
it('should save the new item', async () => { | ||
const id = await save('good', 'item'); | ||
|
||
assert.ok(id); | ||
|
||
const items = await db.getAll(); | ||
|
||
assert.equal(items.length, 1); // ensure save did not create duplicates | ||
}); | ||
}) | ||
``` | ||
|
||
In the above, the first and second cases (the `it`s) can sabotage each other because they are run concurrently and mutate the same store (a race condition): "save"'s insertion can cause the otherwise valid "read"'s test to fail its assertion on items found (and "read"'s can do the same thing to "save"'s). | ||
|
||
## What to mock | ||
|
||
### Modules + units | ||
|
||
```mjs | ||
import assert from 'node:assert/strict'; | ||
import { before, describe, it, mock } from 'node:test'; | ||
|
||
|
||
describe('foo', { concurrency: true }, () => { | ||
let barMock = mock.fn(); | ||
let foo; | ||
|
||
before(async () => { | ||
const barNamedExports = await import('./bar.mjs') | ||
// discard the original default export | ||
.then(({ default, ...rest }) => rest); | ||
|
||
// It's usually not necessary to manually call restore() after each | ||
// nor reset() after all (node does this automatically). | ||
mock.module('./bar.mjs', { | ||
defaultExport: barMock | ||
// Keep the other exports that you don't want to mock. | ||
namedExports: barNamedExports, | ||
}); | ||
|
||
// This MUST be a dynamic import because that is the only way to ensure the | ||
// import starts after the mock has been set up. | ||
// There is a far more technical explanation, | ||
// but just trust that this is logically necessary. | ||
({ foo } = await import('./foo.mjs')); | ||
}); | ||
|
||
it('should do the thing', () => { | ||
barMock.mockImplementationOnce(function bar_mock() {/* … */}); | ||
|
||
assert.equal(foo(), 42); | ||
}); | ||
}); | ||
``` | ||
|
||
### Services | ||
|
||
A little-known fact: node has a builtin way to mock `fetch`. [`undici`](https://github.com/nodejs/undici) is the Node.js implementation of fetch. You do not have to install it—it's shipped with node by default. | ||
|
||
```mjs displayName="endpoints.spec.mjs" | ||
import assert from 'node:assert/strict'; | ||
import { beforeEach, describe, it } from 'node:test'; | ||
import { MockAgent, setGlobalDispatcher } from 'undici'; | ||
|
||
import endpoints from './endpoints.mjs'; | ||
|
||
|
||
describe('endpoints', { concurrency: true }, () => { | ||
let agent; | ||
beforeEach(() => { | ||
agent = new MockAgent(); | ||
setGlobalDispatcher(agent); | ||
}); | ||
|
||
it('should retrieve data', async () => { | ||
const endpoint = 'foo'; | ||
const code = 200; | ||
const data = { | ||
key: 'good', | ||
val: 'item', | ||
}; | ||
|
||
agent | ||
.get('example.com') | ||
.intercept({ | ||
path: endpoint, | ||
method: 'GET' | ||
}) | ||
.reply(code, data); | ||
|
||
assert.deepEqual(await endpoints.get(endpoint), { | ||
code, | ||
data, | ||
}); | ||
}); | ||
|
||
it('should save data', async () => { | ||
const endpoint = 'foo/1'; | ||
const code = 201; | ||
const data = { | ||
key: 'good', | ||
val: 'item', | ||
}; | ||
|
||
agent | ||
.get('example.com') | ||
.intercept({ | ||
path: endpoint, | ||
method: 'PUT' | ||
}) | ||
.reply(code, data); | ||
|
||
assert.deepEqual(await endpoints.save(endpoint), { | ||
code, | ||
data, | ||
}); | ||
}); | ||
}); | ||
``` | ||
|
||
### Time | ||
|
||
Like Doctor Strange, you too can control time. You would usually do this just for convience to avoid artificially protracted test runs (do you really want to wait 3 minutes for that setTimeout to trigger?). You may also want to travel through time. | ||
|
||
```mjs displayName="master-time.spec.mjs" | ||
import assert from 'node:assert/strict'; | ||
import { describe, it, mock } from 'node:test'; | ||
|
||
import ago from './ago.mjs'; | ||
|
||
|
||
describe('whatever', { concurrency: true }, () => { | ||
it('should choose "minutes" when that\'s the closet unit', () => { | ||
mock.timers.enable({ now: new Date('1999-12-01T23:59:59Z') }); | ||
|
||
const t = ago('2000-01-01T00:02:02Z'); | ||
|
||
assert.equal(t, '2 minutes ago'); | ||
}) | ||
}); | ||
``` | ||
|
||
This is especially useful when comparing against a static fixture (that is checked into a repository), such as in [snapshot testing](https://nodejs.org/api/test.html#snapshot-testing). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters