Test-Driven Development (TDD)
Test-Driven Development (TDD)
Test-Driven Development is a discipline, not just a technique. You write a failing test before any production code, make it pass with the smallest possible change, then clean up. This sequence — Red → Green → Refactor — is the heartbeat of TDD. When done consistently it produces code that is correct by construction, minimal in scope, and easy to change because every behaviour is covered by a test the moment it is introduced.
The Red-Green-Refactor Cycle in Detail
- Red — Write a test for the next small piece of behaviour. Run it. It must fail, proving the test is actually exercising something that does not yet exist. A test that passes immediately is not a red step; it is a sign the test is wrong or the behaviour already exists.
- Green — Write the minimum production code required to make the test pass. Resist the urge to write more than the test demands. Hardcoding a return value is legitimate here if it makes the test pass; the next test will force you to generalise.
- Refactor — With all tests green, improve the design: remove duplication, clarify names, extract methods, apply patterns. The test suite is your safety net. If any test goes red during refactoring you have introduced a regression — revert and try again.
A Worked Example: Money Arithmetic
We will TDD a simple Money class that can add amounts of the same currency. We start with nothing but the test file.
Iteration 1 — Red: a Money object knows its amount
Running this fails with a compilation error: Money does not exist. That is the red step.
Iteration 1 — Green: write the bare minimum
The test is now green. We resisted adding add(), validation, or anything else the test does not demand.
Iteration 2 — Red: adding money of the same currency
Red: add() does not exist.
Iteration 2 — Green
Green. Now the refactor step: is the name amount clear? For a record accessor it is fine. No duplication. Move on.
Iteration 3 — Red: mismatched currencies must throw
Iteration 3 — Green
Iteration 3 — Refactor
All three tests pass. We can now extract the guard into a private method to make the intent clearer:
All tests still green. The refactor was safe.
The Triangulation Technique
When the green step is "cheat" (e.g. return 15;), you triangulate by writing a second test that forces a real implementation. For example, a test for add(3, 5) returning 8 and another for add(7, 2) returning 9 forces you to write return a + b rather than hardcode 8.
TDD and the Design Pressure
TDD exerts continuous design pressure. If writing a test is painful — you need to construct many collaborators, reach deep into internals, or mock a static method — the test is telling you the design is wrong. Common smells and their TDD-driven fixes:
- Hard to instantiate — the class has too many responsibilities. Split it.
- Must mock the database in a unit test — the domain logic is coupled to persistence. Introduce a repository interface and inject it.
- Testing a private method — private methods are implementation detail. Test the public contract; if the private logic is complex enough to need its own test, extract it into a collaborator class.
- Test setup is 30 lines long — the subject has too many dependencies. Apply the Single Responsibility Principle.
Applying TDD to a Service Layer
Real-world TDD often involves injected dependencies. The key is to write the test first with a mock, which forces you to define the interface of the collaborator before you implement it.
This test defines the public API of OrderService (placeOrder), the collaborator interfaces (OrderRepository, PaymentGateway), and the expected interaction — all before a single line of OrderService is written.
Summary
TDD is a tight feedback loop: Red → Green → Refactor, repeated many times per hour. It keeps code minimal, forces good design, and ensures every behaviour is tested from the moment it exists. The discipline is hardest to learn at first but becomes natural with practice. The worked examples here — the Money record and the OrderService — show the same pattern at the value-object and service layer. Carry this rhythm into every new feature and you will write less buggy, more maintainable Java.