Chapter 9: Tests Are Gentle Truths
“Plan for the difficult while it is easy, do the great while it is small.”
— Lao Tzu
A typo almost cost me my job.
I was refactoring a billing system, cleaning up some legacy code that had grown tangled over the years. The change seemed straightforward: consolidate how we calculated line items, reduce some duplication, make the logic easier to follow. I was confident. I’d lived in that code for months.
The tests caught it before I did. A handful of assertions turned red, and when I traced through what was happening, I felt my stomach drop. My “cleanup” had introduced a bug that would have multiplied every line item by ten. Not a rounding error. Not an edge case. A tenfold increase in what we charged customers.
If that had shipped, we would have billed our customers ten times what they owed. Some would have noticed immediately. Others wouldn’t have, not until their finance teams reconciled at the end of the month. By then, the damage would have been catastrophic. Refunds, apologies, lost trust, probably lost accounts. And somewhere in the postmortem, my name on the commit.
The tests saved me. Not my careful thinking. Not code review. A few lines of automated verification that ran in seconds and told me, quietly, before anyone else knew: you made a mistake. Fix it.
That’s what tests are: gentle truths. They tell you you’re wrong in private, before the world finds out.
The Skeptic’s Journey
I didn’t always believe in tests.
Early in my career, the idea of writing code to test my code felt redundant. I’d already written the code. I knew what it did. Why write more code to confirm what I already knew? It rubbed against my brain like sandpaper. Extra work for no visible benefit.
I started writing tests because I was told to, not because I understood why. The first few months felt like bureaucracy. Check the box. Satisfy the coverage requirement. Move on to the real work.
Then something shifted. I started using tests to understand systems I hadn’t written. Instead of reading through tangled implementation code, I’d read the tests first. They told me what the system was supposed to do. They showed me the inputs that mattered and the outputs that were expected. They were documentation that couldn’t drift out of sync with reality, because if it did, the build would break.
Later, I started using tests as a safety net. I’d make a change, run the suite, and know immediately if I’d broken something. The fear that used to accompany every refactor began to fade. I could move faster because I could move with confidence.
Eventually, I started using tests to think. Writing the test before the implementation forced me to articulate what I actually wanted. What should this function return? What happens when the input is empty? What happens when the service is down? The questions surfaced before I’d written a line of production code.
The skeptic became a believer, one saved mistake at a time. Tests earned my trust by saving me, over and over, from myself.
The Conversation
When you write a test, you’re speaking to the future. You’re not just verifying that code works today. You’re leaving a message for everyone who will touch this code later: here’s what I intended. Here’s what should happen. Here’s where the edges are.
A good test suite is a map of the system’s behavior. The happy paths are there: when everything goes right, this is what you get. But so are the unhappy paths: when the network fails, when the input is malformed, when the database returns nothing. The tests document not just what the code does, but what the code is supposed to do when things go wrong.
This matters more than most developers realize. Implementation code tells you how. Tests tell you why. They capture intent in a way that comments can’t, because tests are executable. They prove their claims every time they run.
I’ve inherited codebases where the tests were the only reliable documentation. The READMEs were outdated. The wikis were abandoned. But the tests were current, because if they weren’t, they’d fail. They were the living record of what the system actually did.
The Net
There’s a particular feeling when you work in a codebase without tests. Every change feels like defusing a bomb. You make an edit, hold your breath, deploy, and watch the logs for explosions. You move slowly. Fear, not caution, sets the pace.
I remember this feeling from before testing was common. That’s just what we did. Every refactor was a gamble. Every deployment was a prayer. You learned to make small changes and ship them fast, so that when something broke, you had a shorter list of suspects. It worked, mostly. But it was exhausting.
Tests change the texture of the work. When you have a suite you trust, you can move with confidence. You can refactor aggressively, knowing that if you break something, you’ll know immediately. You can experiment, because the cost of a wrong turn is a red test, not a production incident. The fear drains away, and what remains is something closer to play.
Tests provide this net. They don’t eliminate mistakes. They make mistakes cheap. They give you a private space to be wrong, to learn, to adjust, before the consequences become real.
The billing system tests didn’t prevent me from making an error. They caught the error before it escaped. The failure still happened. The net caught it.
Keeping AI Honest
Chapter 8 talked about beautiful code as structural guidance for your AI partner. Tests extend this idea.
Tests are where you tell the truth before the code does.
When you write tests before handing a task to an AI, you’re giving it a roadmap. You’re saying: here’s what success looks like. Here are the inputs, here are the expected outputs, here are the edge cases. The AI doesn’t have to guess what you want. It has a specification it can run against.
The dynamic shifts. Instead of reviewing AI-generated code and trying to spot errors through inspection, you let the tests do the verification. The AI writes code. The tests pass or fail. You iterate until green. The feedback loop is tight and mechanical, which is exactly where machines excel.
But there’s something deeper here. AI can write code faster than any human, but it has no sense of consequence. It doesn’t know which bugs would be catastrophic and which would be minor annoyances. It optimizes for what you ask, not for what you need. Tests are how you encode what you need. They’re the constraints that keep the AI’s output aligned with reality.
I’ve watched AI generate code that looked correct, passed a superficial review, and would have failed spectacularly in production. The tests caught it. The AI wasn’t malicious. It was simply literal. It did what I asked, not what I meant. The tests knew the difference.
In the human-AI team, tests are the shared language of correctness. They’re how you verify that the partnership is producing what it should. Write the tests first, and you’ve given your AI partner something more valuable than instructions. You’ve given it a way to prove it understood.
What Makes a Good Test
Not all tests are useful. Some are noise.
A good test codifies intent. It says: this function should behave this way, given this input. It captures the contract between the code and its callers. When the test fails, you know something meaningful has changed.
Test that an invoice total is correct, not that InvoiceCalculator calls TaxService exactly once.
A bad test codifies implementation. It’s so tightly coupled to how the code works that any refactor breaks it, even when the behavior stays the same. These tests create friction without providing safety. They punish you for improving the code.
A good test is specific about what matters and silent about what doesn’t. It checks the outputs that callers depend on. It ignores the internal details that might change. It’s resilient to refactoring because it’s testing the right thing.
A good test tells a story. You can read it and understand what scenario it’s describing. The setup is clear. The action is obvious. The assertion makes sense. When it fails, you know where to look.
A good test covers the edges. The happy path is necessary but not sufficient. What happens when the input is null? When the list is empty? When the service times out? These are the places where bugs hide, and good tests flush them out.
A test suite that captures meaning ages well. One that captures mechanics becomes a burden.
The Wait
There’s a moment I’ve come to appreciate. You make a change. You run the tests. For a few seconds, you wait.
If the tests pass, you exhale. The change did what you thought it did. You can move forward.
If the tests fail, you pause. Something unexpected happened. Maybe you broke something. Maybe the test is wrong. Maybe your understanding of the system was incomplete. Either way, you’ve learned something, and you learned it here, in the quiet space between your keyboard and the CI server, not in production at 2 AM.
This is the gentleness. Tests don’t judge. They just report. They give you a moment of pause, a breath, a chance to reconsider before the consequences become real. They’re the tap on the shoulder that says: hey, did you mean to do that?
The alternative is finding out from your customers. Or your boss. Or the on-call engineer who got paged at midnight. Tests let you find out from a machine that doesn’t care about your ego, in a context where fixing the problem is still easy.
That kindness is built into the process.
The Living Record
Code rots. Documentation drifts. But tests that run stay honest.
A comment can lie. It can describe what the code used to do, back when someone wrote it, before six developers made changes without updating the prose. You read the comment, trust it, and get burned.
A test can’t lie, not for long. If it claims the function returns a sorted list and the function stops sorting, the test fails. The lie is exposed. Either you fix the code or you fix the test, but you can’t leave the contradiction standing. The build won’t let you.
I read tests first when I’m learning a new codebase. They’re the record that can’t drift. They show me what the system actually does, not what someone once hoped it would do. When I drop an AI into a new repo, the first thing I feed it is the test suite. The implementation tells it how we did it. The tests tell it what we value.
And when I write tests, I think about the person who will read them later. Verification is only part of it. I’m leaving a trail. Here’s what I understood. Here’s what I intended. Here’s where I thought the dragons might be.
The tests are a gift, like beautiful code is a gift. They’re the decision to spend a little more time now so that someone else can understand later. They’re the documentation that stays true.
The Gentle Truth
Tests won’t make you a better programmer. Practice does that. Thinking does that. Tests just make the consequences of your mistakes smaller and the feedback faster.
But that’s enough. That’s more than enough.
The billing system typo would have been a disaster. Instead, it was a lesson I learned in private, fixed in minutes, and never thought about again until I sat down to write this chapter. The tests absorbed the impact. They turned a potential catastrophe into a minor correction.
That’s what gentle truths do. They catch you before you fall. They give you the chance to learn without paying the full price.
Write the tests. Not because someone told you to. Not because the coverage metric demands it. Write them because you will make mistakes, and you’d rather find out from a machine than from your customers. Write them because the person who maintains this code after you deserves to understand what you meant. Write them because your AI partner needs to know what success looks like.
Write them because they’re kind. And kindness, in this work, is rarer than it should be.