Skip to content

Overview — Test-First & Finding the Edge Cases

So far this book has treated tests as something you write after the code, to check the work: you build a function, then you attack its edges. This Part inverts the order. What happens if the test comes first — before the function exists? And once you are writing tests deliberately, what if you stop hand-picking the inputs entirely and let a tool generate thousands of them, hunting the edges for you?

Those two questions define this Part. They sound like tactics — a workflow tweak, a fancier assertion library — but each turns out to be a way of thinking. Writing the test first is a design tool: it forces you to describe what the code should do before you can hide behind how it does it. Generating inputs instead of guessing them is an edge-finding tool: it searches a space too large for any human to enumerate by hand. Both serve the one question this whole book runs on — how do you earn justified confidence that software works, and how do you think about the edges that break it?

Two clarifications before we go. First, everything here is language-agnostic: the loop, the shared language, and the properties are ideas, and the code snippets on later pages (Python, JavaScript, Rust) are illustrations, not prerequisites. Second, “test-first” is a slight simplification for the whole Part — TDD and BDD really do put the test before the code, while property-based testing puts the specification of correct behavior before the choice of which specific inputs to try. In both cases the discipline is the same: decide what “correct” means without leaning on the implementation to define it for you.

The techniques in this Part share a spirit — decide what “correct” means before or independently of the implementation — but they aim at different targets. Keep them distinct:

  • TDD (Test-Driven Development) drives design. You write a failing test, write the least code to pass it, then refactor — the red-green-refactor loop. The tests are a byproduct; the real output is code shaped by having to be testable from the first line. TDD’s central claim is that test-first pressure produces better-decomposed, more decoupled code.
  • BDD (Behavior-Driven Development) builds a shared language. It reframes tests as executable descriptions of behavior — Given / When / Then — written in words a product owner, a tester, and a developer can all read and agree on. BDD’s target is the gap between what was asked for and what was built.
  • Property-based testing finds edge cases. Instead of asserting on one example, you state a property that must hold for all inputs — then a tool generates hundreds or thousands of random inputs trying to break it, and shrinks any failure to its smallest form. Its target is the vast space of inputs you would never think to type by hand.

TDD and BDD are about when and how you write tests; property-based testing is about what the test asserts. They compose freely — you can drive a design with TDD while asserting a property, or phrase a BDD scenario over generated data — but you should be able to say which idea you are leaning on at any moment.

idea primary target one-line claim
───────── ──────────────── ────────────────────────────────
TDD design "write the test first and the
code comes out decoupled"
BDD shared language "describe behavior in words all
three roles agree on"
property edge cases "state a law; let a machine try
thousands of inputs to break it"

A quick way to keep them straight: TDD changes when you write the test (before), BDD changes who can read the test (everyone), and property-based testing changes what the test claims (all inputs, not one).

The central tension: examples vs. properties

Section titled “The central tension: examples vs. properties”

Underneath this Part is one argument the book returns to again and again, and it is worth naming up front because it reframes everything else:

Example-based thinking: you pick the cases. You look at the code, imagine the inputs that matter, and write an assertion for each — sort([3,1,2]) == [1,2,3], sort([]) == []. Every test is a specific, readable, hand-chosen example.

Property-based thinking: the tool picks the cases. You describe a rule that any correct output must obey — “the result of sort is always in non-decreasing order, and is a permutation of the input” — and a generator throws thousands of inputs at it, including ones you would never have imagined.

EXAMPLE-BASED PROPERTY-BASED
you enumerate the inputs you state the invariant
┌───────────────────────┐ ┌───────────────────────┐
│ sort([3,1,2])==[1,2,3]│ │ ∀ xs: │
│ sort([]) ==[] │ │ isSorted(sort(xs)) │
│ sort([5]) ==[5] │ │ isPermutation(...) │
│ ... 6 more you thought│ │ │
│ of on a good day │ │ tool generates 1000s, │
└───────────────────────┘ │ finds [0,-0.0,NaN] │
readable, but only the │ you'd never have typed│
edges YOU imagined └───────────────────────┘

Neither wins outright, and the honest answer — which this Part defends — is that they are complementary. Examples are concrete, readable, and pin down the specific cases you care about (including the ones from bug reports). Properties cover ground no human would, and routinely surface edges — -0.0, NaN, the empty string, a list of one element repeated a million times — that were invisible until the generator found them. The tension between “I choose the cases” and “the tool chooses the cases” is the thread that pulls this Part together.

To feel the difference, take one small function: a parseAmount that turns "$1,000.50" into 100050 cents. The example-based tester writes the cases they can picture — a normal amount, zero, a negative, a malformed string — maybe eight of them, and each is a readable line documenting an intended behavior. The property-based tester writes one invariant instead: “for any amount n, parseAmount(formatAmount(n)) == n” — a round-trip. Now the generator manufactures amounts the human never listed: 0, MAX, an amount with no cents, one with a trailing space, one so large it overflows the intermediate multiplication. If any of those round-trips wrong, the tool shrinks the failure to the smallest offending amount and hands it back. Same function, two philosophies: the first enumerates behaviors, the second states a law and lets a machine attack it. A mature suite for parseAmount usually wants both — the readable examples as documentation, the property as a dragnet.

Test-first is a design tool, not just a bug-catcher

Section titled “Test-first is a design tool, not just a bug-catcher”

It is tempting to file all of this under “finding bugs.” That undersells it. A test written before the code cannot check for a bug in code that does not exist yet — so what is it doing?

It is a specification you can execute. Before you write parseAmount, you write a test that says parseAmount("$1,000.50") should return 100050 cents. In that moment you have been forced to decide: does it return cents or dollars? A number or a Result? What about "$0", "", "abc"? You answer these as design questions, at the cheapest possible time — before a single line commits you to an answer. The test-first techniques in this Part earn confidence not only by catching regressions later, but by making you state the intended behavior first, which is where most defects are actually born. That is the book’s throughline restated: confidence is justified when it rests on a clear, checkable claim about what the code should do — and test-first is a machine for producing that claim.

cost of deciding "what should parseAmount do?"
─────────────────────────────────────────────────► time
▲ cheap expensive ▲
│ │
test-first: decide HERE │
(before any code) ● │
└── you change your mind for free │
code-first: decide HERE ──────────────────────────► ●
(after an implementation, a caller,
and maybe a bug report already assume
the OLD answer — now the change is costly)

There is a second, quieter design effect. To write the test first, the code has to be reachable from a test — which means you cannot bury parseAmount inside a giant function that also reads a file, hits the network, and formats HTML. The test can only call something it can construct in isolation, so test-first pressure keeps pushing units apart: small functions, injected dependencies, seams where a fake can stand in for the real world. Nobody decided “let’s decouple this” as a virtue; the decoupling fell out of the mechanical requirement that the test run before the code around it exists. Page 3, How Writing the Test First Changes the Design, is entirely about this side effect — and page 4 is about the times it goes wrong, when chasing testability produces indirection and mock-heavy tests that are worse than the coupling they replaced.

Under the hood — what a property actually is

Section titled “Under the hood — what a property actually is”

An example test and a property test look similar in code but differ in one structural place: where the input comes from. An example test contains its input. A property test receives its input from a generator and asserts a relationship that must hold regardless of which input arrived.

EXAMPLE TEST PROPERTY TEST
───────────── ─────────────
test("reverse twice") { forAll(list_of(ints)) { xs ->
assert reverse(reverse( assert reverse(reverse(xs))
[1,2,3])) == [1,2,3] == xs
} }
▲ one fixed input, baked in ▲ input arrives from forAll;
the tool runs this body 1000×
with different xs, and if any
body fails it SHRINKS xs to
the smallest failing case

Three moving parts make a property test work, and every later page in this Part is really about one of them:

  • A generator — the source of inputs. list_of(ints), strings, dates_between(...). Good generators bias toward the known edges (0, 1, -1, MIN, MAX, empty, huge) rather than sampling uniformly, which is why a thousand generated cases beat a thousand truly random ones. This is page 7.
  • An invariant — the relationship that must always hold. reverse(reverse(xs)) == xs. decode(encode(x)) == x. isSorted(sort(xs)). Finding an invariant is the hard, creative part; a whole vocabulary of property patterns (round-trip, idempotence, oracle, metamorphic) exists to help, and that is also page 7.
  • Shrinking — when the body fails on some ugly generated input like [8, -3, 8, 0, 8, 8], the tool automatically searches for the smallest input that still fails, often landing on something like [0, 0]. Without shrinking, a failure hands you a haystack; with it, you get the needle. This is page 8.

Notice that the property test does not know the answer for any specific input — it never writes == [1,2,3]. It only knows a rule. That is the trade you make moving from examples to properties: you give up the concreteness of a hand-checked answer and gain a claim that covers the whole space. The examples-vs-properties page (page 6) is about knowing when that trade is worth making.

Read these in order. The first four pages build the test-first habit (TDD) and the shared-language layer on top of it (BDD); the last three shift from examples to properties — the heart of the Part.

#PageWhat it teachesBuilds on
2TDD: The Red-Green-Refactor LoopThe core three-step loop — write a failing test, make it pass minimally, then refactor — and why each step existsThe tester’s mindset; unit tests as the unit of feedback
3How Writing the Test First Changes the DesignWhy test-first pressure pushes code toward small, decoupled, dependency-injected unitsThe red-green-refactor loop (page 2)
4TDD’s Honest Benefits and CriticismsThe real evidence for TDD and the real critiques (over-mocking, test-induced damage, “TDD is dead”)Pages 2–3; the throughline of justified confidence
5BDD and Gherkin: A Shared LanguageGiven / When / Then, executable specifications, and the three-role conversation BDD is really aboutTDD as the mechanical base BDD reframes
6Example-Based vs Property-Based TestingThe Part’s central tension made precise: hand-picked cases vs. a generated space, and when each earns its keepThe examples you have written all book; page 5’s shift toward specifying behavior
7Property-Based Testing: Generators and InvariantsHow to write a property — generators for inputs, invariants that must always hold, and the common property patternsThe examples-vs-properties framing (page 6)
8Shrinking and the Edge-Case SuperpowerWhy shrinking a random failure to its minimal form is what makes property testing usable, and how it surfaces edgesWriting properties (page 7); the edge-cases field guide
900Revision · TDD & BDDA prose recap tying test-first design and property-based edge-finding back to one threadThe whole Part

One question runs through every page, and it is the book’s throughline pointed at the moment before the code exists:

What is the cheapest way to state — and then relentlessly check — what “correct” means, so that the confidence you end up with is justified rather than assumed?

Pages 2–4 answer it with time: state correctness before you implement, and let that pressure shape the design. Page 5 answers it with language: state correctness in words every stakeholder shares. Pages 6–8 answer it with scale: state correctness as a property, and let a machine check it against inputs you would never enumerate. Same question — cheapest honest statement of “correct” — three different levers.

the Part's one question:
"cheapest honest way to state and check what CORRECT means?"
├── lever = TIME (pages 2–4, TDD)
│ state it BEFORE you implement → design falls out
├── lever = LANGUAGE (page 5, BDD)
│ state it in words all three roles share
└── lever = SCALE (pages 6–8, property-based)
state it as a law → a machine checks 1000s of inputs

This Part is not free-standing. It stands on the tester’s mindset — the adversarial habit of asking “how could this break?” — and on the edge-cases field guide, which enumerated, category by category, where the boundaries live. Test-first techniques give you the discipline to write the check before the code; property-based testing gives you a machine that hunts exactly the boundaries that field guide catalogued, without you having to type each one. In that sense pages 6–8 are the field guide’s edges made automatic: instead of you sending the empty list and the MAX + 1, a generator sends them — and a thousand of their neighbors — every run.

It also feeds forward. Once you can state a property, you have a specification precise enough to hand to the higher levels of testing later in the book, and precise enough for the automation and CI machinery to run on every commit. A property that holds for all inputs is the strongest cheap claim a single test can make, which is why this Part sits where it does: after you have learned to see edges, and before you learn to run tests at scale.

book flow around this Part
───────────────────────────────────────────────
tester's mindset → edge-cases field guide → [ THIS PART ] → automation / CI
"how could this "here are the boundaries, "state the check "run every
break?" category by category" first; let a check on
machine hunt the every commit"
boundaries for you"

This Part is not a sales pitch. Every technique here has passionate advocates and serious critics, and both are load-bearing. TDD has been credited with better design and blamed for brittle, over-mocked test suites — a debate we take head-on in page 4. Property-based testing finds edges nothing else does, but writing a good property is genuinely hard and a lazy one gives false comfort. Wherever a technique has a real cost, this Part names it. Justified confidence is not the same as enthusiasm; part of the discipline is knowing exactly what each tool does not give you.

Three misconceptions worth clearing before you start, because they quietly derail people:

  • “Test-first means writing all the tests up front.” No. TDD writes one failing test, makes it pass, and refactors — a tight loop measured in minutes, not a big-design-up-front ritual. Page 2 is about the loop’s rhythm.
  • “BDD is just TDD with different keywords.” The keywords (Given / When / Then) are the surface. The substance is a conversation that pins down behavior before code, in language a non-programmer can check. Miss the conversation and you get the ceremony without the benefit — a criticism page 5 confronts directly.
  • “Property-based testing replaces example tests.” It does not. Properties and examples answer different questions — a law over all inputs vs. a documented specific case — and the strongest suites carry both. That is the whole point of the examples-vs-properties tension.
  1. This Part covers three test-first ideas. Name each one and the target it primarily aims at — design, shared language, or edge cases.
  2. State the difference between example-based and property-based thinking in terms of who picks the inputs, and give one concrete edge a generator might find that a human likely would not type.
  3. A test written before the code cannot catch a bug in code that does not exist yet. So what is a test-first test actually doing, and how does that connect to the book’s idea of justified confidence?
  4. Why does this Part insist on covering the criticisms of each technique, not just the benefits? What does “justified confidence” have to do with that choice?
  5. Using the roadmap, explain how pages 2–4, page 5, and pages 6–8 each answer the Part’s thread question with a different lever (time, language, scale).
Show answers
  1. TDD aims at design — the red-green-refactor loop shapes code by forcing it to be testable from the first line. BDD aims at shared languageGiven / When / Then specs a product owner, tester, and developer can all read. Property-based testing aims at edge cases — asserting a rule over all inputs so a generator can hunt the ones you would miss.
  2. In example-based testing you pick the inputs (you hand-write sort([3,1,2])); in property-based testing the tool picks them from the whole input space, guided toward known boundaries. A generator readily produces edges a human rarely types by hand — e.g. -0.0, NaN, the empty string, or a list of one element repeated a million times.
  3. It is an executable specification: writing the test first forces you to decide the intended behavior (return type, units, handling of ""/"abc", etc.) before implementing. That is where most defects are born, so it earns confidence by making “correct” a clear, checkable claim rather than an afterthought — and confidence resting on such a claim is what justified means.
  4. Because every technique has real costs — TDD can produce brittle, over-mocked suites; a lazy property gives false comfort — and hiding those costs would make the reader over-confident. Justified confidence means knowing precisely what a tool does and does not give you; that requires the criticisms as much as the benefits.
  5. Pages 2–4 → time: state correctness before you implement and let that shape the design. Page 5 → language: state correctness in words every stakeholder shares. Pages 6–8 → scale: state correctness as a property and let a machine check it against inputs no human could enumerate. Same thread question — the cheapest honest statement of “correct” — answered with three different levers.