Overview

5 Testing

Testing is presented as the safety net that lets a codebase evolve without turning every refactor into a gamble. The chapter argues that good tests are not merely about having green checks; they must create real confidence while staying readable and maintainable. A central warning is to avoid putting logic in tests, especially logic that mirrors the production code, because doing so can reproduce the same bug in both places and give a false sense of correctness. Tests should favor simple, explicit expectations that make their intent obvious.

The chapter also explains that coverage metrics are useful but limited. Line coverage shows which statements were executed, while branch coverage reveals whether different decision paths were exercised; both can help expose gaps, but neither guarantees effective testing. Property-based testing is introduced as a complementary technique that checks general rules or invariants across many generated inputs instead of relying only on handpicked examples. This helps uncover edge cases humans may not think of and can apply from small functions up to broader system-level properties such as consistency, reliability, and performance expectations.

For unit tests, the chapter emphasizes practical maintainability: good tests should be focused, understandable, fast, deterministic, isolated, informative, resilient, and free from unnecessary delays or external dependencies. Unit tests also act as living documentation when they clearly describe expected behavior and edge cases. The most important guidance is to test behavior rather than implementation details, so refactoring internals does not break tests unnecessarily. Finally, test-driven development is treated as a useful personal workflow for some situations, not a universal requirement or measure of quality; what matters most is whether the resulting tests actually validate the software and can be trusted.

Summary

  • Logic in tests breeds bugs. Tests should be simple and declarative. Adding loops, conditionals, or complex calculations to test code increases the risk of reproducing the very bug you're trying to catch.
  • Hardcoded expected values beat computed ones. Where possible, write the expected result as a literal rather than computing it from the inputs. Computed expectations let the test silently inherit any flaw in the logic it's meant to verify.
  • Coverage is a metric, not a target. While high coverage is generally good, strictly enforcing a number (e.g. 80%) often leads to low-quality tests written solely to satisfy the gatekeeper — Goodhart's Law in action.
  • Branch coverage reveals hidden gaps. Line coverage can report 100% even when a specific false-branch path crashes. Branch coverage is a more rigorous metric, ensuring every path through a control structure is verified.
  • Property-Based Testing validates invariants. By defining properties that must always hold (e.g. "debits must equal credits"), PBT can uncover edge cases that manual example-based tests routinely miss.
  • Tests must focus on behavior. To ensure tests survive refactoring, verify the public API's output rather than its internal state or private methods.
  • TDD is a workflow, not a quality gate. Test-Driven Development is a personal tool for structuring thought. Whether a developer writes the test before or after the code is irrelevant; what matters is the quality and maintainability of the final test suite.

FAQ

Why should tests avoid logic that mirrors the code under test?

Because duplicating the same logic in the test can duplicate the same bug. If the production code and the test both compute the expected value using the same flawed formula, the test may pass while the code is still wrong. Prefer hardcoded expected values when possible, and keep tests simple enough that readers do not need to mentally recompute the result.

What should a good test generally do and avoid?

A good test should use clear inputs, hardcoded expected results when practical, and simple assertions that make the intended behavior obvious. It should avoid mirroring the implementation, adding loops or complex calculations, and hiding the expected result behind logic that could itself be wrong.

What does code coverage actually measure?

Code coverage usually refers to line coverage: the percentage of code statements executed while running tests. It is useful because it provides an objective signal, but it does not prove that the tests are meaningful, complete, or capable of catching important bugs.

Should teams enforce a strict code coverage percentage?

The chapter argues against treating a fixed percentage as a hard quality gate. Coverage is worth tracking, but rigid targets can lead developers to write tests just to satisfy the number. Judgment matters more than a universal threshold because different projects, files, and features require different levels of testing.

What is the difference between line coverage and branch coverage?

Line coverage measures whether statements were executed during tests. Branch coverage checks whether each possible branch of a condition was exercised, such as both the true and false paths of an if statement. A test can produce 100% line coverage while still missing an untested branch that contains a bug.

What is Property-Based Testing?

Property-Based Testing, or PBT, focuses on general rules that should always hold instead of manually selected input-output examples. Rather than asking, “For this input, do I get this output?”, PBT asks, “For any valid input, does this property remain true?” The test framework then generates many inputs to try to break the property.

How is fuzzing related to Property-Based Testing?

Fuzzing can be seen as a subset of Property-Based Testing. Fuzzing usually feeds random inputs into software to find crashes or unexpected behavior, often using a broad property like “the program should not crash.” PBT goes further by defining specific correctness properties the software must always satisfy.

What are examples of properties used in Property-Based Testing?

Common PBT properties include structural properties, such as preserving length; idempotency, where applying an operation multiple times gives the same result; commutativity, where argument order does not matter; and roundtrip properties, where encoding then decoding returns the original value. At a broader system level, properties can include latency, consistency, financial balance, or retry safety.

What makes a good unit test?

A good unit test is focused, understandable, maintainable, informative, fast, deterministic, resilient, isolated, local, and free of sleep statements. These qualities make tests easier to trust, easier to read, and less likely to become a maintenance burden as the code evolves.

Why should unit tests focus on behavior instead of implementation?

Behavior-focused tests validate what the code does through its public API, not how it does it internally. This makes tests more maintainable and less brittle. If an internal data structure or private method changes but the observable behavior stays the same, behavior-focused tests should not need to change.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Coder Cafe ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • The Coder Cafe ebook for free