Unit Testing
On this page
What Unit Tests Should Cover
Unit tests focus on small pieces of logic: parsing, validation, domain rules, permission checks, calculations, and mapping between types. They should run in milliseconds and require no network, filesystem, or database.
Design for Unit Testing
The easiest way to write unit tests is to structure code around pure functions and small modules. Push I/O to the edges: HTTP handlers, DB adapters, filesystem, message queues. Keep core logic independent.
TypeScript Example: Pure Function
export type CreateUserInput = { email: string; name: string };
export function validateCreateUser(input: unknown): CreateUserInput {
if (!input || typeof input !== 'object') throw new Error('Invalid body');
const x = input as any;
if (typeof x.email !== 'string') throw new Error('Invalid email');
if (typeof x.name !== 'string') throw new Error('Invalid name');
return { email: x.email.trim().toLowerCase(), name: x.name.trim() };
}
Test Outcomes, Not Internals
A unit test should assert the observable behavior: return values, thrown errors, and transformed outputs. Avoid asserting private functions, internal call order, or exact error strings unless you explicitly treat them as part of your contract.
Example Unit Test
import test from 'node:test';
import assert from 'node:assert/strict';
import { validateCreateUser } from './validateCreateUser';
test('normalizes email and name', () => {
const out = validateCreateUser({ email: 'Test@Example.com ', name: ' Alice ' });
assert.deepEqual(out, { email: 'test@example.com', name: 'Alice' });
});
test('rejects non-object body', () => {
assert.throws(() => validateCreateUser(null), /Invalid body/);
});
Production Anti-Patterns
- Unit tests that hit the database or network (they become slow integration tests)
- Randomized data without seeds (flaky failures)
- Assertions on timestamps or ordering without deterministic control