Testing
Tests are an executable specification. They are not about catching bugs — they're about enabling change. A codebase with bad tests is harder to refactor than one with no tests at all.
The test pyramid
Three layers, in inverse proportion to their cost:
- Unit (base) — a single function/class in isolation. Milliseconds per test. ~70% of the suite.
- Integration (middle) — multiple components together: API + database, service + queue. Seconds. ~20%.
- End-to-end (tip) — the system as a user touches it. Minutes. ~10%, only for critical happy paths.
Invert the pyramid (lots of slow E2E, few unit) and your suite takes 40 minutes, fails flakily, and gets disabled within a sprint.
Properties of good tests
FIRST:
- Fast — millisecond range. Slow tests don't get run.
- Isolated — no shared state between tests.
- Repeatable — same result every run, every machine.
- Self-validating — pass or fail; no manual inspection.
- Timely — written with (or before) the production code.
Coverage as a floor
Line coverage measures "did this line run during tests?" — not "is this line correct?" A test that calls a function and asserts nothing has 100% coverage and zero value.
Treat coverage as a floor (the assignment requires 70%) and combine with:
- Mutation testing — flip operators, see if tests still pass. If they do, your tests don't actually test anything.
- Branch coverage — did each
ifbranch execute?
Testing microservices
End-to-end is doubly painful across services: deploy 7 services, seed each, fail at step 5 because of a flaky DNS lookup. Instead:
- Contract tests — service A publishes the contract it expects, service B verifies it produces messages matching that contract. Tools: Pact.
- Component tests — test a single service with its dependencies stubbed (testcontainers-style).
- Reserve E2E for one or two critical user flows.
See Microservices testing (sub-deck).
TDD as a tool, not a religion
Red → green → refactor. Useful when:
- The behaviour is well-defined (algorithms, parsers, business rules).
- You want the test to drive a clean API design.
Less useful when:
- You're exploring an unknown problem (do a spike first, then write tests for what you keep).
- The "unit" requires elaborate mocking — that's a smell about the design.
"Coverage is the seatbelt, not the destination." High coverage is necessary but not sufficient — the question is whether your tests would catch a regression.
What to remember at exam time
- Pyramid: unit / integration / E2E with rough proportions.
- FIRST: name each letter.
- Coverage is necessary, not sufficient. Pair with mutation or branch metrics.
- Microservices favour contract + component tests over E2E.