The Journey: From Coverage Metrics to Behavior-First
Part 9 of 9 in the series: Unit Testing — A Behavior-First Approach
Reading Time: ~8 minutes
Looking Back
This series started with a simple observation: our tests were slowing us down instead of helping us ship. Coverage metrics looked good on paper, but developers dreaded making changes. Every refactor meant updating dozens of test files. Every PR was cluttered with noise that had nothing to do with behavior.
The root cause was never testing itself. It was how we thought about testing. I know that sounds like a "No True Scotsman" argument: "Your tests aren't the problem, you're just not writing real tests." But that's not what I mean. The patterns in this series aren't a purer form of testing. They're a different orientation. We stopped asking "how do I test this code?" and started asking "what behavior am I verifying?"
Over the past eight posts, one principle kept surfacing: tests are not a verification tool for finished code. They're a communication system. Every change to a test should signal something important. If a test is changing, the public interface or requirements changed. That's the throughline of this entire series.
The Problems We Encountered
- Brittle Tests — Adding a property to
Taskbroke 50+ tests across 18 files - Tests Coupled to Implementation — Refactors broke tests even when behavior hadn't changed
- Hard-to-Read Tests — Full of setup code and direct property assertions
- No Documentation Value — Tests didn't explain why code existed
The Solutions We Discovered
- Builders and Test DSL — Isolated test setup. Adding a new property required updating one builder, not dozens of test files.
- Behavior-Focused Assertions — Assertion chains that test outcomes, not implementation. Tests survive refactoring.
- Readable Test Structure — Given/When/Then structure and use case comments. Anyone can read and understand tests.
- Tests as Living Documentation — Executable specifications that stay in sync with code automatically.
What Changed
The difference was night and day. We went from dreading refactors to welcoming them. Test files stopped showing up in PRs unless requirements actually changed. Adding a property to a model meant updating one builder, not hunting through dozens of files. Coverage stayed high naturally because TDD was driving the implementation, not because anyone was chasing a number.
The test suite went from being something the team worked around to something the team worked through. It became the first thing we read in a PR, the first thing we showed new hires, and the first thing we pointed AI agents to when onboarding them to a feature.
Key Takeaways
1. Tests Are a Communication System
Every change to a test should signal something: either the public interface changed, the requirements changed, or you discovered a bug. If a test is changing and behavior isn't, something is wrong with your test.
2. If a Test Is Changing, the Public Interface or Requirements Changed
This is the principle that drives everything else. When you internalize this, you stop testing private implementation and start thinking about contracts.
3. Coverage Metrics Are Dangerous in Isolation
"80% test coverage" is easy to measure but doesn't guarantee quality. Focus on behaviors, not coverage percentages.
4. Test Every Behavior, Not Every Class
Testing every class creates fragile, overfitted tests. Testing every behavior creates resilient, maintainable tests.
5. TDD Creates Design Pressure
Writing tests first forces you to design clean APIs. The test becomes a showcase for the API.
6. Tests Are Contracts for AI Agents
In a world where AI writes most code, tests become the primary communication mechanism. Well-written tests enable effective supervision.
The Benefits of a Test-First Culture Done Well
Culture is patina. It builds slowly, layer by layer, through repeated effort and reinforcement. A test-first culture doesn't happen overnight, and it doesn't happen by mandate. It happens when the team experiences the benefits firsthand and starts to trust the process.
I still work in codebases that have both old and new patterns side by side. When I have to touch older code that was written with the "test every class" mentality, I immediately miss the quality of life improvements that behavior-first tests give. I end up spending a lot more time understanding what is being tested and what the code does instead of why a feature or behavior was worth shipping.
Compare that to the parts of the codebase where behavior-first testing is in place. I can jump into a domain I haven't touched in months, scan the test file names, and immediately understand what features exist and what behaviors are expected. prioritize-task.test.ts, block-task.test.ts, assign-task.test.ts. That's not just organization. That's communication. My experience as a developer is fundamentally better when the tests tell me what a domain is supposed to do before I ever open a source file.
The Mindset Shift
From: "How do I test this code?" To: "What behavior am I trying to verify?"
From: "What methods does this class have?" To: "What should this system do?"
From: "How do I achieve 80% coverage?" To: "How do I verify behaviors that matter?"
Next Steps
If you're starting this journey:
- Start with one feature — Apply behavior-first testing and see the difference
- Adopt TDD — Write tests first, even if it feels slow at first
- Use Test DSL — Build builders and assertion chains for your domain models
- Review tests in PRs — Make tests the first thing you review
- Teach AI agents — Provide Test Guidelines to AI coding assistants
An Honest Note
None of this is easy. If you're working in a shared codebase with established patterns, some of this may be impossible to implement without significant buy-in. I'm not pretending otherwise.
I wrote a lot of bad tests. I paid a lot of money to other people asking them to write bad tests. It took years before I could say I genuinely enjoy testing. That's not a humble brag. It's context for how long this took to click.
I know that pulling out Java programmer recommendations from Robert Martin, Kent Beck, and Martin Fowler in 2026 may not be the most popular move. I've seen the hot takes and the dunking. But every time I come back to their work and try to actually build something with their advice, I learn a bit more and unlock a bit more. I thought I understood it the first time. I didn't. I thought I understood it the third time. I was closer. The ideas are simple to state and difficult to internalize, and I think that gap is where most of the frustration comes from.
I'm always searching for ways to be better, both in my programming and in my leadership. This entire series is a manifestation of that: recognizing that something I was doing and leading others to do wasn't working, and figuring out a better way forward. If you're looking for where to start, I'd recommend Kent Beck's Test-Driven Development: By Example and Robert Martin's Clean Architecture. There is a lot of wisdom to be found in both.
I hope this series helps you or your team write more readable, more maintainable tests. If you're interested in talking about architecture, testing, or any of the patterns covered here, please reach out.
Complete Series
- When Tests Become a Liability — The problem that started it all
- Builders and Test DSL — How to isolate test setup
- Assertion Chains — Beyond direct property checking
- Layer of Behavior — Testing behaviors, not classes
- Living Documentation — What tests should communicate
- Behavior-First Mindset — Why mindset matters more than tools
- Tests as PR Documentation — Reading tests to understand change
- TDD with AI — Supervising agents through tests
- The Journey — From coverage metrics to behavior-first (this post)