There is a persistent myth that tests slow you down. That they are overhead, a chore, something you bolt on after the real work is done. Teams under pressure skip them. Engineers in a hurry promise to add them later. Later never comes.
But the teams that move fastest — genuinely fastest, measured in value delivered, not lines written — are almost always the ones with strong test suites. Not because testing is virtuous, but because it is practical. Tests let you change code with confidence. Without them, every change is a gamble.
Tests as documentation
A well-written test suite is the most accurate documentation your codebase has. It describes what the system actually does, in executable form, verified on every commit. Comments lie. Documentation drifts. Tests, if they pass, are true by definition.
When a new engineer joins the team and wants to understand how the billing system works, the test suite is the first place they should look. Not the wiki page from two years ago. Not the Slack thread that someone bookmarked. The tests. They show the inputs, the expected outputs, and the edge cases that someone already thought about.
Write tests as if someone who has never seen the codebase will need to read them. Because they will.
The testing pyramid
Most teams get this backwards. They write a handful of unit tests and a mountain of end-to-end tests that are slow, flaky, and expensive to maintain. The testing pyramid says the opposite:
- Many unit tests — fast, isolated, cheap. Test individual functions and classes. These should run in milliseconds and give you immediate feedback on whether your logic is correct.
- Some integration tests — test how components work together. Database queries, API endpoints, service interactions. Slower than unit tests, but they catch the bugs that unit tests cannot: the ones that live in the seams between components.
- Few end-to-end tests — test critical user journeys through the entire system. These are expensive to write, slow to run, and prone to flaking. Use them sparingly, for the paths that absolutely must work.
The pyramid works because it optimizes for feedback speed. When you break something, you want to know in seconds, not minutes. Unit tests give you that. Integration tests fill the gaps. End-to-end tests are the safety net for the scenarios that matter most.
Testable code is well-designed code
If your code is hard to test, that is not a testing problem. It is a design problem.
Code that is tightly coupled, that reaches into global state, that mixes business logic with infrastructure concerns — this code resists testing because it resists understanding. When you refactor it to be testable, you are also refactoring it to be maintainable.
Dependency injection is not an academic exercise. It is what allows you to swap a real database for an in-memory one in your tests, cutting your test runtime from minutes to seconds. Pure functions are not a functional programming fetish. They are functions you can test without setting up the world first.
Writing tests first — or at least thinking about tests before you write the code — forces you to design interfaces before implementations. It forces you to think about what a function should do, not how it should do it. This is a design activity disguised as a testing practice.
When not to test
Testing has diminishing returns, and good engineers know where the line is.
Do not test:
- Trivial code — getters, setters, simple data transformations with no branching logic. If the test is more complex than the code it tests, you have gone too far.
- Framework behavior — do not test that your ORM correctly saves to the database. The framework authors already tested that. Test your queries, not their library.
- Implementation details — test what the code does, not how it does it. If you refactor the internals and every test breaks even though the behavior is unchanged, your tests are too coupled.
The purpose of tests is confidence. Enough confidence to deploy on a Friday afternoon. Enough confidence to refactor a critical path. Enough confidence to hand the codebase to someone new and know that they will not accidentally break it.
Write the tests that give you that confidence. Skip the ones that do not.