Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 24
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 5
CI/CD 6
  • GitHub Actions
  • Slack Reporting
QA Strategy 4
Case Studies 5
Back to Bookshelf
§Book Summary

Growing Object-Oriented Software, Guided by Tests

Foreword by Kent Beck

by Steve Freeman & Nat Pryce

Buy on Amazon
§In One Sentence

Start with a walking skeleton that builds, deploys, and tests end-to-end, then grow your software one feature at a time — letting tests drive the design of loosely coupled, well-structured objects.

What This Book Is About

The preface describes it as "a practical guide to the best way we've found to write object-oriented software: test-driven development." The book covers the processes the authors follow, the design principles they strive for, and the tools they use. It is "also very much about design" — the authors argue TDD works best when taken as a whole, not as isolated practices.

The title uses "growing" because the approach is incremental: "We have something working at all times, making sure that the code is always as well-structured as possible and thoroughly tested." They quote John Gall: "A complex system that works is invariably found to have evolved from a simple system that works." It uses "guided" rather than "driven" because "the technique still requires skill and experience."

The TDD Cycle: Two Feedback Loops

The book describes TDD as two nested feedback loops. The outer loop starts with a failing acceptance test for the feature you're building. While it fails, it shows the feature isn't done. The inner loop is the familiar unit-test cycle: write a failing test, make it pass, refactor.

Outer Loop (Acceptance Tests)

  • Exercises the system end-to-end
  • Written in domain language, not technical terms
  • Measures progress — feature is done when it passes
  • Becomes a regression test once passing

Inner Loop (Unit Tests)

  • Write a failing test → make it pass → refactor
  • Supports the developer, should run fast
  • Uses mock objects to test objects in isolation
  • Failing unit tests should never be committed

The book's golden rule: "Never write new functionality without a failing test."

Start with a Walking Skeleton

The book's most distinctive concept. Their definition: "A 'walking skeleton' is an implementation of the thinnest possible slice of real functionality that we can automatically build, deploy, and test end-to-end." The application functionality should be "obvious and uninteresting" so the team can focus on getting the infrastructure right.

They define "end-to-end" as including the deployment process, not just the code: "nothing forces us to understand a process better than trying to automate it." The walking skeleton should be drawable "in a few minutes on a whiteboard." They warn this is not Big Design Up Front: "We're making the smallest number of decisions we can to kick-start the TDD cycle."

"We have seen projects canceled after months of development because they could not reliably deploy their system. We have seen systems discarded because new features required months of manual regression testing and even then the error rates were too high."

The authors note teams are "frequently surprised by the time it takes" to get a walking skeleton working. The book says this flushes out "unknown unknown" technical and organizational risks "while there's still time, budget, and goodwill to address them."

Object-Oriented Design: Messages Over Classes

The book focuses on communication between objects rather than class hierarchies. They quote Alan Kay: "The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be." An OO system is "a web of collaborating objects" whose behavior is "an emergent property" of how objects are composed.

"Tell, Don't Ask" — The calling object describes what it wants in terms of the role its neighbor plays, letting the called object decide how. The book shows a "train wreck" chain of getters collapsed into a single meaningful method call: master.allowSavingOfCustomisations() instead of navigating five levels of internal structure.

Object Peer Stereotypes — The book categorizes an object's relationships into three types: Dependencies (services it needs to function — pass through constructor), Notifications (peers it informs about changes — fire-and-forget), and Adjustments (peers that tune its behavior — like the Strategy pattern).

"No And's, Or's, or But's" — You should be able to describe what an object does without conjunctions. If you need "and" or "or," the object should probably be split into collaborators, one for each clause.

Ports and Adapters — Business domain code is isolated from technical infrastructure. Interfaces describe relationships in the application's own terms (ports), and bridges to each technical domain are adapters. The authors say they arrive at this architecture "almost automatically by just following the code and taking care to keep it clean."

Only Mock Types That You Own

The book gives several reasons not to mock third-party code (though it acknowledges exceptions for hard-to-trigger behavior like throwing exceptions):

1

You often don't fully understand how third-party code works. Documentation may be incomplete or wrong. You won't know if your mock actually matches the real behavior.

2

You can't change the external API, so you can't respond to design feedback from tests that use it. "Whatever alarm bells the unit tests might be ringing about the awkwardness of an external API, we have to live with it."

3

Tests that mock external libraries "often need to be complex to get the code into the right state." The mess tells you the design isn't right, but you can't fix the external code.

The solution: write a thin adapter layer that uses the third-party API to implement interfaces defined in your application's domain. Test these adapters with focused integration tests. This produces "a set of interfaces that define the relationship between the application and the rest of the world in the application's own terms."

Listening to the Tests

The book's central design feedback mechanism. Their definition: "Sometimes we find it difficult to write a test for some functionality we want to add to our code. In our experience, this usually means that our design can be improved." They always check whether it's an opportunity to improve production code before making the test more complicated.

Bloated constructor? — A long argument list signals hidden structure. Look for arguments always used together (package them into a new object) or arguments with the same lifetime.

Can't replace a dependency? — The book uses new Date() calling System.currentTimeMillis() as an example. The fix: introduce a Clock and pass it in. Making the dependency explicit forced them to discover a missing domain concept.

Too many expectations? — "When everything looks equally important, we can't tell what's significant." The fix: separate stubs (using allowing) from expectations (using one). Only side-effecting actions should be strict expectations.

Test suite looks confused? — "If we can break up the test class into slices that don't share anything, it might be best to go ahead and slice up the object too."

The book states: "The qualities that make an object easy to test also make our code responsive to change." And warns: "Unit-testing tools that let the programmer sidestep poor dependency management in the design waste a valuable source of feedback."

Writing Tests That Last

Part IV of the book covers how to keep tests from becoming a maintenance burden. The authors state: "We need to apply as much care and attention to the tests as we do to the production code."

Test Readability

Name tests after behavior, not methods. The book introduces the TestDox convention where test names read as sentences: "A List holds items in the order they were added." Include the expected result, action, and motivation.

Test Data Builders

Use the builder pattern instead of Object Mother. A builder has safe defaults, chainable methods for overriding, and a build() method. Only the builder breaks when constructors change, not every test.

Test Diagnostics

"The point of a test is not to pass but to fail." Design tests to fail with clear messages. Use self-describing values like UNUSED_CHAT = null so failure messages explain themselves.

Test Flexibility

The single rule: "Specify Precisely What Should Happen and No More." Test for information, not representation. Use "Allow Queries; Expect Commands." Only enforce invocation order when it matters.

Testing the Hard Stuff

Part V covers three topics teams commonly struggle with:

Persistence — Clean up at the start of each test, not at the end. Reject the common technique of rolling back transactions (it doesn't test what happens on commit). Make transaction boundaries explicit with a "transactor" helper object.

Threads — Separate functional behavior from concurrency policy. Pass in an Executor — in tests use a deterministic one, in production use a real one. Then write stress tests that run multiple threads to expose synchronization errors. "Watch the test fail" and tune until it reliably fails before adding synchronization.

Async code — "An asynchronous test must wait for success and use timeouts to detect failure." Two strategies: listen for events (fastest) or poll for state changes. Key warning: "Allowing flickering tests is bad for the team. It breaks the culture of quality."

10 Things You Can Use in Your Framework

1

Start every project with a walking skeleton. The book defines it as "the thinnest possible slice of real functionality that we can automatically build, deploy, and test end-to-end." Get the build/deploy/test pipeline working before writing real features.

2

Write a failing acceptance test before each feature. The book says to use only domain language, not technical terms. When the test passes, the feature is done. Failed acceptance tests that used to pass mean a regression.

3

Only mock types you own. Write adapter layers for third-party code. Test adapters with integration tests. Mock only your own interfaces in unit tests.

4

When a test is hard to write, fix the production code first. The book says: "The qualities that make an object easy to test also make our code responsive to change." Hard-to-test code is hard-to-change code.

5

Classify each object's peers as dependencies, notifications, or adjustments. Dependencies go through the constructor. Notifications are fire-and-forget. Adjustments tune behavior (like the Strategy pattern). This guides how you design and test each relationship.

6

Use test data builders instead of Object Mother. The book says Object Mother "does not cope well with variation" and becomes "too messy to support." Builders have safe defaults, chainable overrides, and only the builder breaks when constructors change.

7

Name tests after behavior, not methods. The book rejects names like testBidAccepted(). Instead: "A List holds items in the order they were added." Include the expected result, the action, and why.

8

Separate functionality from concurrency. Pass in an Executor. In tests, use a deterministic single-threaded one. In production, use the real thing. The book says: "Concurrency is a system-wide concern that should be controlled outside the objects."

9

For persistence tests, clean up at the start, not the end. The book rejects rolling back transactions in tests because "it doesn't test what happens on commit." Data left behind after a test helps diagnose failures.

10

"Specify Precisely What Should Happen and No More." The book's single rule for test flexibility. Allow queries, expect commands. Don't enforce invocation order unless it matters. Assert only the relevant properties, not the entire object.

Quotes from the Book

"Never write new functionality without a failing test."

"The domain model is in these communication patterns."

"We 'pull' interfaces and their implementations into existence from the needs of the client, rather than 'pushing' out the features that we think a class should provide."

"When writing a test, we ask ourselves, 'If this worked, who would know?'"

"Code isn't sacred just because it exists, and the second time won't take as long."

"An interface describes whether two components will fit together, while a protocol describes whether they will work together."

Who Should Read This

  • SDETs building test automation frameworks — The adapter pattern, test data builders, and async testing techniques are directly applicable to any framework you build.
  • Developers doing TDD — The preface says the book is for "developers with professional experience who probably have at least looked at test-driven development." It shows how TDD drives design, not just catches bugs.
  • Teams struggling with brittle tests — Part IV on sustainable TDD (readability, diagnostics, flexibility) addresses exactly why test suites become a maintenance burden.
  • Anyone working with mock objects — The preface says the original motivation was "to finally explain the technique of using mock objects, which we often see misunderstood."

§ Verdict

8 / 10

The worked example in Part III (Chapters 9-19) is the heart of the book — it shows the full TDD process on a real application including XMPP messaging, Swing UI, and multithreading. It is detailed enough to see the wrong turns and backtracking that happen in real development. Parts I, II, and IV contain the concepts you'll reference most often. Part V on persistence, threads, and async is worth reading but more specialized. The Java/jMock/Swing examples are dated, but the design principles and testing patterns apply to any language or framework.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.