Someone has to own this


Let's start with a question you've been avoiding: do you actually do TDD, or do you nod along at the conference talk, then go home and write the implementation first like everyone else? Be honest. The tests aren't listening. The uncomfortable truth is that TDD has always demanded a very particular flavour of masochism: sit down, write code that deliberately fails, resist every instinct to just make the thing work, and trust that the scaffolding of failure will eventually produce something beautiful. Most developers respect this in the same way they respect flossing. And now here comes Claude, ready to help. What could go wrong?
Quite a lot, as it happens, and in a rather specific way. Left to its own devices, Claude will skip the red phase entirely. It's too helpful. You ask it to write a test for a feature that doesn't exist yet, and, like an eager intern who's read ahead, it writes the test and the implementation in the same breath, hands them to you simultaneously, and waits for praise. The tests pass, obviously, because Claude just wrote both sides of the conversation. This is the AI equivalent of marking your own homework, then putting a gold star on it. SD Times put it this way: AI doesn't eliminate TDD, it exposes whether you understand it. Which is a polite way of saying it will cheerfully help you do it wrong if you let it.
The solution is that TDD and AI are actually a spectacular match, provided you hold the leash. The cognitive barrier that kills TDD in practice is that writing a test requires defining the interface, which requires understanding the architecture, which requires mentally sketching the implementation anyway; the circle eats itself before you've typed a single assertion. AI dissolves that barrier completely. Let Claude draft the test, you review it, then let Claude implement against it, one cycle at a time, not in bulk.
In other words, use this exact prompt:
"Sit. Good boy. Now, write one failing test for the login function. Just one. Don't implement anything. Don't even think about the implementation. I can see your little cursor twitching. Stop that. Write the test, run it, show me it's red, and then sit back down. If you've written only the test and nothing else, you'll get a biscuit."
Or, if you're feeling boring, something like this:
Now, some of you will have noticed a tension and are feeling very pleased with yourselves about it. If we're telling Claude to stop after every single test like an overexcited Labrador, when exactly does the plan happen? Surely we want Claude to think before it acts? Yes, probably. The thing is, plan mode and TDD aren't quite solving the same problem (or at least, that's how it seems to me). Plan mode is about what you're building. TDD is about whether you actually built it. One is a map. The other is the slightly neurotic habit of checking you're still on the road every five minutes, which feels excessive right up until the moment you aren't.
The temptation is treating a good plan as a green light to let Claude implement the whole thing in one uninterrupted sprint. My suspicion is that this works right up until it doesn't, and when it doesn't, you're several pull requests deep into something that's coherent, confident, and subtly wrong in ways that are deeply tedious to unpick. The plan tells Claude where it's going. TDD, is just the thing that keeps it from lying to you about whether it got there. You probably want both: plan mode to decide what you're building, TDD to keep Claude honest while it builds it. But I'll admit I'm still working out exactly where the seams should be, and anyone who tells you they've fully figured this out is either very clever or writing a different blog.
His code is like a Rolex.
You look at his gherkin, and smile. It says what it's testing, in plain English, and there's nothing else on the page to confuse or distract you... just English.
"Why then, did our unit tests give me a feeling of dread?" I asked myself. It was quite obvious, that even though we used a neat, little NuGet package, called BDDfy to allow us to write English gherkins, our code was littered with private methods and other necessary evils:
"If we could just hide all the non-gherkin stuff, that would make the code more pleasing to read, understand, and work with," I thought to myself. So I suggested to my team that we use a partial class. We put the public methods, with the English definition of the tests in one file, and everything else in another. The team loved the idea, so that is what we do.
Now, we still have many tests in a file (See how I organize unit tests here). So, the question is, are the tests still useful, to understand what the system does?
Let me put it another way... how do you know enough?
The company I work for, IHS Markit, is merging with S&P Global. The combined company will have a hundred gazillion staff members, but only 6 divisions. How do you know what S&P do? You list the 6 divisions.
The universe consists of 200,000,000,000,000,000,000,000 stars. Perhaps it can be summarized as a bag of galaxies.
It takes many years to learn about the human body, but you can summarize it as five things: head, neck, body, a pair of arms and a pair of legs.
Simplifying complexity into a tree, where each node has about 5 children, makes everything easy to understand.
Microservices is the modern, cool way to develop distributed systems. AWS S3 consists of 300 services. I haven't seen this architecture, but I imagine if they're just a list of services that call each other higgledy piggledy, then AWS has a big problem. Our software has 30 services, and even that is a lot to try to understand.
My point is, that I think pretty much everything in software, and perhaps even life, needs to be broken down into a tree, limited to about 5 child nodes per parent to make it easy to understand. Now, don't get me wrong... 5 is more of a thumb suck than anything else, but 5 is a handful.You may be wondering, well, what if it can't be broken down?
Problems with flat, rather than code organized in hierarchies, adding to complexity are:
Normally we wouldn't organize classes in a flat structure. We'd use a hierarchy of folders. I think it would be interesting if a class or namespace was a folder. Perhaps an IDE plugin could be written. It might look like this:
Would limiting methods to 5 per class or interface help to make it easier to understand? The interface segregation principle suggests keeping interfaces "small" to prevent having classes that have to implement methods unnecessarily. It may require a little less thought, to have alarm bells going off in your head when you reach 5 methods, to consider whether another interface is required.
Can limiting properties to 5 per class make them easier to work with? I've seen some pretty large view models. Code becomes much cleaner when they're broken down into a tree structure. It can be tricky to change it later, so best to keep them small from the start.
Can limiting lines to 5 per method make the logic easier to follow? Uncle Bob says, "extract, extract, extract." If you follow that, your methods will be pretty small. Using the number 5 might be too artificial, but when you have to scroll to see your whole method, you've probably gone way too far.
I've heard a suggesting that methods should only have 3 parameters. The suggestion is that if there are more, they should be in a class.
Disclaimer: As I've only thought of some of these ideas recently, I haven't experimented with these ideas very much. So, I'm going to try them out, and see how it goes.
Thoughts?
Image credits:
https://www.freeimages.com/photo/skeleton-pocket-watch-back-1637098
https://www.freeimages.com/photo/nebula-space-astro-photo-astronomy-sky-1420873
https://www.freeimages.com/photo/burning-tree-1377053
https://www.freeimages.com/photo/dave-in-window-1553933
![]() |
| Nerdy Monk with Kittens |
![]() |
| DRY is not WET |
![]() |
| DROP is WET |
![]() |
| YAGNI... unless it's a kitten! |
![]() |
| Chaos from multiple responsibilities |
When a single change to a program results in a cascade of changes to dependent modules, that program exhibits the undesirable attributes that we have come to associate with “bad” design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. It says that you should design modules that never change. When requirements change, you extend the behavior of such modules by adding new code, not by changing old code that already works.[1]
We've all tried AI by now. Some promote a build as quickly as you can approach, while others look at me in horror, as if using AI extens...