Testing

4 lessons

Types of tests

Unit tests check one small piece of code (a function or class) in isolation and run in milliseconds. Integration tests check that several parts work together, for example your code plus a real database or a real HTTP client against a running service. End-to-end (E2E) tests drive the whole system the way a user would, through the UI or the public API, and are the slowest and most fragile.

Contract tests check that a service and its consumer agree on the shape of an API (fields, types, status codes), so each side can be tested and deployed without running the other. A quick way to classify a test: ask what it talks to. Only in-process code means unit; a real external component means integration; the whole deployed system means E2E.

Check yourself

A test starts the service with a real PostgreSQL container, saves an order through the service code and reads it back. What kind of test is this?

Test doubles: stub, mock, fake, spy

Test doubles replace real dependencies in tests. A stub returns canned answers ("getUser always returns Alice") and never checks how it was used. A mock is pre-programmed with expectations, and the test fails if the expected calls do not happen ("sendEmail must be called exactly once").

A fake is a working but simplified implementation, such as an in-memory repository instead of a real database. A spy wraps a real object: the real calls still happen, and the spy records them so the test can check them afterwards. For example, wrap the real email sender in a spy, run the sign-up code, then assert that send was called once with the new user's address.

A dummy is passed only to satisfy a signature and is never used. Rule of thumb: use a stub to supply data, a mock or spy to verify behaviour, a fake when you need realistic behaviour without the real infrastructure.

Check yourself

A test must fail if the payment gateway is NOT called exactly once with the order total. Which test double is designed for this?

The test pyramid and TDD

The test pyramid describes a healthy distribution: many fast unit tests at the bottom, fewer integration tests in the middle, and a small number of slow end-to-end tests at the top. An inverted pyramid (mostly E2E tests) produces suites that take hours and break for unrelated reasons. Test-driven development (TDD) follows a short loop: write a test that fails (red), write the simplest code that makes it pass (green), then improve the code while keeping all tests green (refactor).

Writing the test first proves that the test can actually detect the missing behaviour, which a test written afterwards cannot guarantee.

Check yourself

A team practises TDD and wants to add password-strength validation. What is the correct first step?

Flaky tests and coverage

A flaky test sometimes passes and sometimes fails with no code change. Usual causes: fixed sleeps and timing assumptions, shared mutable state between tests, dependence on test execution order, real network or clock calls, and unseeded randomness. The fix is to remove the nondeterminism (wait for an explicit condition, isolate state, seed the randomness, fake the clock) rather than to retry or raise the timeout.

Code coverage measures which lines or branches ran during the tests. It says nothing about whether the assertions are meaningful: a test that calls every function and asserts nothing gives 100% coverage and catches no bugs. Coverage is useful for spotting untested areas, not for proving correctness.

Check yourself

A test suite reports 100% line coverage. Which statement is guaranteed to be true?