Testing strategies
Automated tests are a safety net that lets developers change code with confidence. Different kinds of tests check different things, and good tests are fast, focused, and reliable.
The testing pyramid
The testing pyramid suggests many fast unit tests at the base, fewer integration tests in the middle, and a small number of slow end-to-end tests at the top. Unit tests check one function or class in isolation; integration tests check components working together, such as code and a database; end-to-end tests drive the whole application like a user would.
Anatomy of a good test
A clear pattern is arrange, act, assert: set up the data, perform the action, and check the outcome. Each test should check one behaviour, have a descriptive name, such as test_withdraw_rejects_overdraft, run quickly, and give the same result every time. Tests that fail randomly, flaky tests, erode trust and should be fixed or removed.
Choosing test cases
Test the normal case, boundary values, and invalid input. For a function accepting ages 0 to 120, test typical values such as 30, boundaries such as 0, 120, -1, and 121, and wrong types. Equivalence partitioning groups inputs that should behave alike so one example from each group is enough. Bugs cluster at boundaries.
Test doubles
Unit tests isolate code from slow or unpredictable dependencies, such as networks, databases, and clocks, using test doubles. A stub returns fixed answers; a mock also records how it was called so the test can verify interactions; a fake is a simple working implementation, such as an in-memory database. Overusing mocks can make tests brittle and tied to implementation details.
Coverage
Code coverage measures what fraction of lines or branches tests execute. Low coverage reveals untested areas, but high coverage does not prove correctness: tests can run code without checking results properly. Coverage is a useful guide, not a goal in itself.
Test-driven development and regression tests
In TDD, tests are written before the code, driving design in small steps. Whatever the process, when a bug is found, first write a test that reproduces it, then fix the code: the regression test ensures that particular bug never returns unnoticed.
Continuous integration
Continuous integration (CI) automatically builds the project and runs its tests whenever code is pushed, using services such as GitHub Actions or Jenkins. If tests fail, the team is alerted before the change is merged or released. CI makes testing routine rather than something people remember to do.
Worked example
Testing a function grade(score) that returns "pass" for 50 to 100 and "fail" for 0 to 49, raising ValueError otherwise. Good cases: 75 gives pass, 20 gives fail, boundaries 49 gives fail and 50 gives pass, 0 and 100 are valid, and -1 and 101 raise ValueError. Eight short tests cover every partition and boundary.
Key takeaways
The testing pyramid favours many unit tests, fewer integration tests, and few end-to-end tests. Good tests follow arrange-act-assert, check one behaviour, run fast, and are deterministic; fix flaky tests. Test normal values, boundaries, and invalid input, using equivalence partitions. Stubs, mocks, and fakes isolate dependencies; avoid brittle over-mocking. Coverage guides but does not prove correctness. Write a regression test for every bug. CI runs tests automatically on every change.
Flashcard
Many unit, fewer integration, few end-to-end tests
Check your understanding
A function accepts quantities from 1 to 99. Which set of test inputs best targets boundaries?
