🎯 Chapter Insight
Testing is often treated as the final step of development.
First, we design the feature. Then we write the implementation. Once everything appears to work, we add a few tests to confirm what we have already built.
But testing should not be something we remember only after the code exists.
Pragmatic developers think about verification from the beginning. Before implementing a feature, they ask what correct behaviour looks like, how it can be observed, and how they will know when the work is complete.
This changes testing from a separate quality check into part of the design process.
Tests provide confidence that the software behaves as expected. They preserve knowledge about requirements, reveal unclear responsibilities, and make future changes safer. A good test does more than catch defects. It explains what the system promises to do.
Testing is not simply about proving that today’s code works. It is about creating the confidence to change that code tomorrow.
💡 Developer Lens
In everyday engineering, tests serve several purposes at once.
They catch regressions when existing behaviour changes unexpectedly. They document how a component should respond to different inputs. They help developers understand unfamiliar areas of the codebase. They also create a safety net for refactoring, optimisation, and future feature development.
A test might explain that an order cannot be submitted without a delivery address. Another might demonstrate how a user’s permissions affect access to a resource. A third might describe what should happen when an external service becomes unavailable.
These tests are executable specifications.
Unlike documentation that may become outdated, tests are repeatedly checked against the running implementation. When they fail, they reveal a disagreement between what the software is expected to do and what it currently does.
This makes tests one of the most reliable forms of living documentation available to a development team.
A developer reading a well-written test should be able to understand the behaviour being protected without studying every implementation detail behind it.
Testing Influences Design
Thinking about tests early often improves the structure of the production code.
Code that is difficult to test is frequently difficult to use, understand, or maintain. A method may have too many responsibilities. A class may depend on hidden global state. Business logic may be tightly coupled to a database, network service, or framework.
When you ask how a piece of behaviour can be tested, these design problems become visible.
To make the code easier to verify, you may separate business decisions from infrastructure. You may replace hidden dependencies with explicit ones. You may break a large operation into smaller components with clearer responsibilities.
The goal is not to distort the design merely to satisfy a testing framework. The goal is to recognise that testability often reflects good modularity.
A component with clear inputs, observable outputs, and focused responsibilities is usually easier to test because it is also easier to reason about.
Testing therefore becomes more than validation. It becomes feedback about the design itself.
Test Behaviour, Not Implementation
Tests become fragile when they depend too heavily on internal implementation details.
Imagine a test that verifies every private method call, temporary value, and internal interaction used to calculate a result. The test may pass today, but even a harmless refactoring could break it despite the observable behaviour remaining correct.
This discourages developers from improving the code. Instead of providing confidence, the test suite becomes another source of resistance.
Strong tests focus on behaviour.
They describe the input, action, and expected outcome from the perspective of the code’s contract. They care that a valid order is accepted, not which private method performs the validation. They care that an expired token is rejected, not how many internal objects participate in that decision.
This does not mean implementation-focused tests are never useful. Certain interactions with databases, queues, or external systems may need to be verified directly. But those details should be tested intentionally rather than accidentally.
The closer a test is tied to meaningful behaviour, the more freedom developers have to improve the implementation behind it.
Tests Create Confidence to Change
Software that cannot be changed safely becomes increasingly expensive.
Without reliable tests, every modification carries uncertainty. A developer fixes one problem but worries about creating another. Refactoring is postponed. Dependencies remain outdated. Features are added through workarounds because changing the underlying design feels too risky.
Eventually, the team spends more time protecting the existing system than improving it.
A good test suite changes this relationship.
It does not guarantee that defects will never occur, but it provides fast feedback when important behaviour changes. Developers can restructure a module, replace an algorithm, or upgrade a dependency while receiving evidence that the system still fulfils its responsibilities.
That confidence affects how teams work.
Small improvements become routine. Technical debt can be addressed incrementally. New contributors can explore the codebase with less fear. Releases become less dependent on manual verification and individual memory.
Tests reduce the cost of change by replacing uncertainty with feedback.
Not Every Test Provides Equal Value
A large number of tests does not automatically mean a system is well protected.
Tests can pass while important behaviour remains unverified. They can duplicate the same scenarios, depend on unrealistic mocks, or assert details that do not matter to users. They can also become slow enough that developers avoid running them.
The goal is not maximum test count. It is meaningful confidence.
A valuable test protects behaviour that matters. It clearly describes what is expected, fails for a useful reason, and remains stable when unrelated implementation details change.
Pragmatic developers consider the purpose of every test.
What risk does it address? What behaviour does it document? What defect would it catch? Would a future developer understand why it exists?
A focused test suite that protects important contracts is more useful than thousands of tests that merely execute lines of code.
🧭 Reflection
When you begin implementing a feature, do you first think about how you will prove that it works?
Do your tests explain the expected behaviour of your system, or do they mostly mirror its implementation?
Could a new developer understand an important business rule by reading the tests around it?
Consider the parts of your codebase that are difficult to change. Is the challenge caused only by complicated code, or also by the absence of reliable tests?
A system becomes easier to evolve when every meaningful change is supported by evidence that existing behaviour remains intact.
⚙️ Practical Tip
The next time you implement a feature, begin by defining how you will verify it.
Before writing the implementation:
- Describe the behaviour in concrete terms
- Identify the important success and failure scenarios
- Decide which outcomes should be observable
- Write tests around the contract rather than internal details
- Keep each test focused on one meaningful expectation
Once the test clearly expresses what success looks like, write the simplest implementation that satisfies it.
Then improve the design while keeping the tests green.
This approach does not mean every line must be written through strict test-driven development. It means verification should be considered part of the work, not something postponed until the feature appears finished.
A strong test suite is one of the best investments you can make in the long-term health of your software.
It builds confidence before features and preserves that confidence as the system evolves.
🔢 #41 of 53 | The Pragmatic Programmer Series
This post is part of my 53-week series summarizing The Pragmatic Programmer, one timeless principle each week, translated into modern software practice and reflection.








