
A campaign offers a 10% discount, but nobody has decided whether the minimum includes the boundary itself, whether a cap applies or what happens to fractions of a cent. You start creating campaign and coupon classes. By the first test, you have made several decisions the requirement never justified.
Kent Beck's Test-Driven Development: By Example earns its place on this list by directing attention to the next verifiable behavior. I recommend studying it as a practice of decision and feedback: state an expectation, confront it with execution and reorganize code when there is a basis for doing so. That discipline remains useful without turning every problem into a unit-testing exercise.
This review covers the first edition. Addison-Wesley/Pearson's listing records publication on November 8, 2002, and a 2003 copyright. The structure comprises a multicurrency money example, construction of an xUnit framework and a section of TDD patterns.
I suggest following the sequence of decisions rather than trying to reuse the implementations. Ask which uncertainty each example resolves and whether the next step would be equally clear in your project. That comparison makes reading an active exercise.
Begin with a scenario list. Turn one item into an executable test, make it and previous tests pass, then consider refactoring. Beck clarifies this sequence in Canon TDD, published in 2023. The list can grow as you discover further questions.
My application of that guidance separates three questions. Does red show that the intended behavior is missing? Does green solve the case without concealing the failure? Does refactoring preserve the contract while improving structure? An import error does not demonstrate that the test detects an incorrect discount. Reach the relevant failure before drawing that conclusion.
A case that starts green may still document a requirement or expose a mistaken assumption about existing code. However, it does not record the same evidence as an implementation that failed and then worked. Investigate why it passed; do not change an expectation merely to manufacture the desired sequence of colors.
For this review's fictional coupon, first define the contract: a nonnegative subtotal in integer cents; an active coupon applying from 2,000 cents; a 10% discount capped at 500 cents. Integer division discards fractions of a cent. These are teaching decisions for this exercise and would require business validation in a real product.
That definition raises useful questions. Does the subtotal include earlier discounts? Does the promotion apply to tax? Could a return produce a negative value? Here those concerns stay outside the function, and invalid subtotals are rejected. Stating this boundary is more honest than adding speculative parameters for unknown rules.
Select cases that force different decisions. A subtotal below the minimum asks about eligibility. Exactly 2,000 asks whether the boundary qualifies. A larger amount asks about calculation; another needs the cap. An inactive coupon asks about enabling the policy. Repeating examples along the same path may enlarge the suite without clarifying your next decision.
In the chapter published as Equality for All, Beck demonstrates a temporary constant implementation and triangulation: another example can demand a more general solution. He also explains using triangulation when the direction of generalization is uncertain.
In our example, returning zero passes the first below-minimum test. The exact-boundary case requires 200, so a constant no longer suffices. This contrast makes the missing rule visible. If the implementation is already clear, writing it directly can be more useful than performing artificially tiny steps.
The criterion is explaining what each step revealed. Reduce the change when a condition confuses you. When you understand the operation, proceed while retaining the tests. A temporary constant must disappear when the contract requires variation; surrounding it with exceptions for individual inputs would memorize examples instead of implementing the rule.
Free and paid products to turn ideas into a foundation you can actually ship.
13 products availableContinue exploring similar topics

A review of Refactoring's second edition: small steps, tests and contracts, with a runnable example, criticism and a practical reading plan.

Use Michael Feathers' approach to begin changes in untested code, identify dependencies and capture existing behavior.

Apply Michael Nygard's approach to timeouts, isolation and recovery through a controlled failure exercise.
A 47-point checklist to find bugs, security risks, and performance issues before launch.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
The original block below represents the final green state rather than every intermediate stage. It requires Python 3 and only the standard library. Save it as coupon_tdd.py and run python3 coupon_tdd.py without -O, which disables assertions.
def discount_cents(subtotal_cents: int, coupon_active: bool) -> int:
if type(subtotal_cents) is not int or subtotal_cents < 0:
raise ValueError("subtotal must be non-negative integer cents")
if not coupon_active or subtotal_cents < 2000:
return 0
return min(500, subtotal_cents // 10)
cases = [
(1000, True, 0),
(2000, True, 200),
(3000, True, 300),
(9000, True, 500),
(3000, False, 0),
(2009, True, 200),
]
for subtotal, active, expected in cases:
assert discount_cents(subtotal, active) == expected
for invalid in (-1, 1.5, True):
try:
discount_cents(invalid, True)
except ValueError:
pass
else:
raise AssertionError(f"accepted invalid subtotal: {invalid!r}")
print("6 discount cases and 3 invalid inputs passed")
Expected output:
6 discount cases and 3 invalid inputs passed
The 2,009-cent case checks discarded fractional cents. Validation rejects True as a subtotal because Python booleans subclass integers. The precondition for coupon_active is a boolean value; this function does not validate HTTP input. An input adapter must establish that contract.
To reconstruct development, initially retain only the first case, implement zero and add the second. Watch the expectation of 200 fail before introducing eligibility and calculation. Then add the cap, activation, fractions and invalid inputs, one behavior at a time. The complete block already passes; on its own, it cannot establish that your process followed TDD.
The calculation needs a subtotal and activation state, not a persisted cart or payment client. This boundary makes testing inexpensive and the contract readable. If calculating a discount requires creating an account, querying inventory and authenticating a session, investigate where commercial decisions became attached to transport details.
That observation does not justify an interface for every operation. The function above needs no mocks, hierarchy or framework. Adding layers without a concrete problem would make the example worse. Design improves when necessary dependencies become explicit and understanding a change requires less work.
Prefer expectations about results to expectations about every internal call. An equivalent calculation should preserve the cases. A genuinely contractual effect order, such as persisting a reservation before publishing confirmation, deserves its own test and an explanation of why the order matters.
A misunderstanding shared by implementation and expectation may go unnoticed. If marketing intends a 2,500-cent threshold and you write 2,000 on both sides, the suite stays green. Examples approved by someone who understands the promotion provide a different check from consistency between your own artifacts.
The exercise also ignores concurrency. Two orders might try consuming a coupon's final available use simultaneously. A pure function protects no counter, transaction or uniqueness constraint. A discount unit test must coexist with tests of consumption policy and evidence about how storage resolves the race.
Integrations add their own boundaries: a provider may round differently or reject a combination of discounts. Contract and integration tests must observe those agreements. Reading Beck helps identify the next behavior, but it cannot replace security experiments, failure analysis or validation with users.
The strength is making small decisions open to discussion. You can ask why a particular test comes next, why generalization happens now or why an abstraction should wait. For paired study, this visibility offers more than debating TDD as a personal preference.
The examples have deliberately bounded scope. They do not include negotiating an ambiguous rule across several teams or making an old system testable. A reader expecting a recipe that automatically discovers good architecture may become overconfident. Choosing the testing boundary still requires knowledge of the system.
The pace also needs adaptation. Mechanically reproducing every step can feel slow without transferring learning to a project. The useful exercise is explaining when to reduce step size and when a direct implementation suffices. Test count and execution frequency cannot compensate for weak expectations or a poorly chosen boundary.
I recommend the book to developers familiar with functions or objects who have already written assertions. Beginners can study it alongside small exercises, but need not learn programming, modeling and testing tools simultaneously. Someone struggling to execute a function independently can establish that foundation first.
For an unprotected codebase, Working Effectively with Legacy Code helps find an observable entry point. Its characterization tests discover existing behavior; our coupon starts with desired expectations. Confusing these goals encourages copying current results as though they were new requirements.
Refactoring complements improving structure while preserving behavior. If your team's suite is slow or unstable, reserve effort for the feedback environment. Reading Beck will not accelerate remote queries or repair shared fixtures.
Use a short session to reconstruct the coupon and record the order of cases. In another session, raise the minimum from 2,000 to 2,500 cents. The case (2000, True, 200) should detect the implementation change before its expectation is deliberately updated to zero. Add cases for 2,499 and 2,500, specifying their results under the new rule. Observe whether the change forces you to rebuild unnecessary dependencies.
Then apply the cycle to a small work-related rule with a colleague reviewing. Record domain questions discovered, structural edits and checks remaining outside local tests. Evaluate those decisions without inventing productivity targets based on a teaching exercise.
I recommend the book for learning to turn uncertainty into a concrete next check. Its expected return is control over decision size and available evidence. You have used the reading well when you can select a case that clarifies the contract and explain, even with a green suite, which risks remain open.