Testing standards and methodology for writing reliable, maintainable tests
Write tests that verify behavior, follow consistent structure, run deterministically, and document requirements through their names.
Test what a function does, not how it does it.
Why: If a refactor breaks your test but not the behavior, your test was wrong. Implementation-coupled tests become maintenance anchors that resist improvement.
Don't:
// Bad: Tests implementation
test("calls processOrder internally", () => {
const spy = jest.spyOn(service, "processOrder")
service.submitOrder(order)
expect(spy).toHaveBeenCalled()
})
// Good: Tests behavior
test("submitted order appears in order history", () => {
service.submitOrder(order)
const history = service.getOrderHistory()
expect(history).toContainEqual(expect.objectContaining({ id: order.id }))
})
Every test follows the same three-part structure.
Why: Consistent structure makes tests scannable. A reader can instantly find the setup, the action, and the expectation.
test("returns 404 when user not found", async () => {
// Arrange — set up preconditions
const db = createTestDb()
const api = createApp(db)
// Act — perform the action under test
const response = await api.get("/users/nonexistent-id")
// Assert — verify the outcome
expect(response.status).toBe(404)
expect(response.body.error).toBe("User not found")
})
Rules:
expect calls are fine if they verify the same concept)beforeEachTests must produce the same result every time, on every machine.
Why: Flaky tests erode trust. A test suite that occasionally fails teaches developers to ignore failures.
Sources of non-determinism to avoid:
Date.now() or setTimeout directly — inject a clockMath.random() — use seeded generators// Bad: Non-deterministic
test("token expires after 1 hour", () => {
const token = createToken()
expect(token.expiresAt).toBeGreaterThan(Date.now()) // Depends on wall clock
})
// Good: Deterministic
test("token expires after 1 hour", () => {
const now = new Date("2026-01-01T00:00:00Z")
const token = createToken({ now })
expect(token.expiresAt).toEqual(new Date("2026-01-01T01:00:00Z"))
})
A test name should describe the requirement, not the implementation.
Why: When a test fails, the name is the first thing you read. A good name tells you what broke without reading the code.
Format: should [expected behavior] when [condition]
// Bad: Describes implementation
test("test validateEmail function")
test("email validation test 1")
test("it works")
// Good: Describes requirement
test("should reject email without @ symbol")
test("should accept email with subdomain")
test("should return 401 when token is expired")
test("should retry failed request up to 3 times")
Rules:
should or describe the expected outcome| Double | Use When |
|---|---|
| Stub | You need to control what a dependency returns |
| Mock | You need to verify a side effect occurred |
| Fake | You need a lightweight working implementation |
| Fixture | You need realistic test data |
Rule of thumb: Only mock at system boundaries (network, database, file system). Internal modules should use real implementations.
tests/
├── unit/ # Fast, isolated, no I/O
├── integration/ # Multiple modules working together
└── e2e/ # Full system tests
Before completing your tests, verify: