Test doubles
| Hand fakes | |
| Stub functions | |
| Record calls | |
| Control time simply |
Hand fakes
A fake is a tiny working substitute for a slow dependency. Instead of a real database, pass a small class that stores rows in memory behind the same method names. The product code cannot tell the difference, and the test runs in milliseconds:
import assert from "node:assert/strict";
interface Store {
save(text: string): void;
all(): string[];
}
class MemoryStore implements Store {
private rows: string[] = [];
save(text: string): void {
this.rows.push(text);
}
all(): string[] {
return this.rows;
}
}
const store: Store = new MemoryStore();
store.save("a");
assert.equal(store.all().length, 1);
The interface is the contract and the fake is one honest implementation of it. Swap the real store for MemoryStore in tests, and every read and write stays fast and repeatable.
Stub functions
A stub is a function that returns a canned answer. When the product needs a network fetch, hand it a stub that returns fixed data, and test the logic around the call instead of the network:
import assert from "node:assert/strict";
function stubUser(): { name: string } {
return { name: "Ada" };
}
function greet(load: () => { name: string }): string {
return "hi " + load().name;
}
assert.equal(greet(stubUser), "hi Ada");
greet takes any loader with the right shape, so the stub slides in without special wiring. The greeting logic is tested, and the network never wakes up.
Record calls
A spy records how it was called. Wrap the dependency in a function that pushes each argument into an array, then assert on that array after the run. The test proves not only the result but the conversation:
import assert from "node:assert/strict";
const calls: string[] = [];
const spy = (text: string): void => {
calls.push(text);
};
spy("hello");
assert.deepStrictEqual(calls, ["hello"]);
The calls array is the recorded history: its length tells how often the spy ran, and its items tell with what. Start the array empty in every test so histories never mix.
Control time simply
Time makes tests flaky when code reads the real clock. Control it simply: pass the current time as a plain number argument instead of calling for the time deep inside the logic. The test sends morning and afternoon values, and every run repeats exactly:
import assert from "node:assert/strict";
function greeting(hour: number): string {
if (hour < 12) {
return "morning";
}
return "day";
}
assert.equal(greeting(9), "morning");
assert.equal(greeting(15), "day");
No timers are mocked and nothing sleeps. Pure values in, plain strings out, and midnight never surprises the suite at midnight.
Next: CI tests
Article author: Arthur Isaev