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

The Art of Unit Testing

Third Edition — with examples in JavaScript

by Roy Osherove with Vladimir Khorikov

Buy on Amazon
§In One Sentence

A unit test checks one exit point of a unit of work — use stubs for incoming dependencies and mocks for outgoing ones, keep no more than one mock per test, and make every test trustworthy, maintainable, and readable.

What This Book Is About

The preface tells a failure story: Osherove's team practiced full TDD but their tests became brittle, broke with every code change (even when the code was correct), and were abandoned within six months. "The project was a miserable failure because we let the tests we wrote do more harm than good. They took more time to maintain and understand than they saved us in the long run."

The 3rd edition switched from .NET/C# to JavaScript and TypeScript with Jest as the testing framework. It expanded trustworthiness, maintainability, and readability into three separate chapters, and added a new chapter on testing strategies. The book uses both vanilla JavaScript (for dynamically typed examples) and TypeScript (for statically typed, object-oriented examples).

The Three Exit Points

The book's central model. A "unit of work" is "all the actions that take place between the invocation of an entry point up until a noticeable end result through one or more exit points." Every unit of work has one or more of these exit points:

1 Return Value

The function returns a useful value. The book says these "should be the easiest exit points to test. You trigger an entry point, you get something back, and you check the value you got back."

2 State Change

A noticeable change to the state or behavior of the system before and after invocation. The book says these "usually require a little more gymnastics. You call something, and then you do another call to check something else."

3 Third-Party Call

A call to a third-party system the test has no control over (logger, email service, API). The book says "we have the most hoops to jump through... that's where we're forced to use things like mock objects."

The author's rule of thumb: "I try to mostly use either return-value-based or state-based tests. I try to avoid mock-object-based tests if I can, and usually I can. As a result, I usually have no more than 5% of my tests using mock objects for verification."

Stubs vs. Mocks: The Core Distinction

The book is emphatic that these are different things, even though "people still tend to use the word 'mock' for anything that isn't real." The distinction is based on the direction of information flow:

Stubs (Incoming Dependencies)

  • Provide fake behavior or data into the code under test
  • You do not assert against stubs
  • You can have many stubs in a single test
  • They are "waypoints, not exit points"

Mocks (Outgoing Dependencies)

  • Fake modules/objects/functions you assert were called
  • Represent an exit point in a unit test
  • "If we don't assert on it, it's not used as a mock"
  • No more than one mock per test

The book gives three reasons to maintain this distinction: readability (test names become generic if mixing concerns), maintainability (asserting against stubs couples tests to internal implementation), and trust (multiple mocks cause "assertion roulette" where the first failure hides all others).

Three Pillars of Good Tests

Trustworthy

When it fails, you're genuinely worried. When it passes, you feel relaxed. You don't need to manually debug "just in case." The book says to avoid logic in tests: "The chances of having bugs in your tests increase almost exponentially as you include more and more logic in them."

Maintainable

Tests should fail only for reasons you care about. Use factory functions instead of beforeEach. Don't test private methods. Avoid overspecification: "An overspecified test contains assumptions about how a specific unit under test should implement its internal behavior."

Readable

Tests are "stories you tell the next generation of programmers." Every test name must convey three pieces: the entry point, the scenario, and the expected behavior (the USE convention). Separate asserts from actions. Avoid magic values.

On the tension between these: "When push comes to shove, trust should trump maintainability. What good is a highly maintainable test that I cannot trust?"

Testing Async Code

The book presents two patterns for making async code unit-testable:

Extract Entry Point. Split async code into two pieces: the async orchestration (stays intact) and the processing logic (extracted as a separate, synchronous function). Test the logic function directly with fast, synchronous unit tests. The book says: "We'd still want an integration test for the original entry point, but not more than one or two of those."

Extract Adapter. Wrap the async dependency behind a simplified interface. In tests, replace the adapter with a synchronous fake. Connected to the Interface Segregation Principle: "Imagine a database dependency with dozens of functions hidden behind an adapter whose interface might only contain a couple of functions."

Testing Strategy: Test Recipes

The book argues against three antipatterns: end-to-end only (diminishing returns — the first E2E test gives the most confidence, each additional one costs the same but adds far less), low-level only (not enough confidence the application works as a whole), and disconnected tests (separate teams writing tests at different levels without coordination).

The proposed solution is test recipes: a simple 5-20 line list written by at least two people — "hopefully one with a developer's point of view and one with a tester's point of view" — just before coding starts. Each scenario maps to a specific test level. Rule of thumb ratio: "For one E2E test, you might end up with five or more lower-level tests."

The book also recommends splitting pipelines into delivery (blocking — go/no-go for release) and discovery (non-blocking — performance, code analysis, shown on a dashboard).

10 Things You Can Use in Your Framework

1

Keep no more than one mock per test. The book says having multiple mocks "means you're testing more than one requirement in a single test." Multiple stubs are fine.

2

Name tests with USE: Unit, Scenario, Expectation. Example: "verifyPassword, given a failing rule, returns errors." The book says good names let you understand a failure from the build log without reading the code.

3

Use factory functions instead of beforeEach. The book says beforeEach "tends to be the garbage bin of the test file" and causes "scroll fatigue." Factory functions keep each test self-contained.

4

Remove logic from tests. No if, for, switch, try/catch, or string concatenation. "Avoid dynamically creating the expected value in your asserts; use hardcoded values when possible."

5

Don't test private methods. The book says "When you test a private method, you're testing against a contract internal to the system." There's always a public method up the chain — test that instead.

6

Separate asserts from actions. The book says: "For the sake of readability and all that is holy, avoid writing assertions and the method call in the same statement."

7

Eliminate magic values. Use named constants like SUNDAY = 0 and NO_RULES = []. Change irrelevant string inputs to "anything" to signal the reader they don't matter.

8

For flaky tests: fix, convert, or kill. Fix by controlling dependencies. Convert by replacing real dependencies with stubs (lowering the test level). Kill if the cost of keeping it exceeds the value. The book warns against the sunk cost fallacy.

9

Use toContain or toMatch instead of toEqual for strings. Strings are "a form of user interface" that change often. Check only the core content, not the exact format.

10

Write test recipes before coding. Sit with one other person for 5-15 minutes. List scenarios and assign each to a test level. Aim for a 1:5 or 1:10 ratio between levels. "If all these tests passed, I'll feel pretty good about this feature working."

Quotes from the Book

"If we don't assert on it, it's not used as a mock."

"The chances of having bugs in your tests increase almost exponentially as you include more and more logic in them."

"When push comes to shove, trust should trump maintainability. What good is a highly maintainable test that I cannot trust?"

"Code coverage shouldn't ever be a goal on its own. It doesn't mean 'code quality.' In fact, it often causes developers to write meaningless tests that will cost even more time to maintain."

"It's unfortunate that people still tend to use the word 'mock' for anything that isn't real, such as 'mock database' or 'mock service.' Most of the time they really mean they are using a stub."

Who Should Read This

  • JavaScript/TypeScript developers new to testing — The book uses Jest throughout with clear, progressive examples. It starts from scratch and builds up to advanced patterns.
  • Teams with brittle test suites — The three pillars chapters (trustworthiness, maintainability, readability) directly address why test suites become a burden and how to fix them.
  • QA engineers writing automation — The stubs-vs-mocks distinction, factory function patterns, and async testing strategies are directly applicable to test framework development.
  • Team leads introducing unit testing — Chapter 11 covers organizational adoption with specific tactics: pilot projects, champions vs. blockers, experiments, metrics, and answers to tough questions from management.

§ Verdict

7 / 10

The stubs-vs-mocks distinction and three exit points framework (Chapters 1, 3-4) are the book's strongest contributions — they give you a clear mental model for deciding how to test anything. The three pillars chapters (7-9) are practical and full of anti-patterns you'll recognize from your own codebase. The organizational adoption chapter (11) is useful but more relevant for leads than individual contributors. Chapter 12 on legacy code is thin — for that topic, the book itself recommends Michael Feathers' Working Effectively with Legacy Code. The JavaScript/Jest examples are modern and accessible, though some patterns (module injection, monkey-patching in the appendix) feel more like workarounds than best practices — which the author himself acknowledges.

§ 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.