← BACK TO THE FEED
DISPATCH #53TestingProgrammingAI/ML

Confession: I Hated TDD. Then I Learned How to Make AI Do It.

Confession: I Hated TDD. Then I Learned How to Make AI Do It.

I used to treat TDD like a chore, but AI has transformed it into my most powerful development tool. By delegating the heavy lifting of test-writing to LLMs, I have reclaimed my role as an architect rather than a code-babysitter.

For the first decade of my career, I treated Test-Driven Development (TDD) like eating vegetables. I knew it was supposedly good for me, I told junior developers they should probably do it, and in my day-to-day workflow, I avoided it at all costs. Writing tests first felt like walking through wet cement. I wanted to build the feature, see it work, and then maybe, if the PR required it, slap some unit tests on at the end to satisfy the coverage threshold.

Then, Large Language Models entered my IDE, and my entire relationship with testing flipped upside down. I don't just tolerate TDD anymore. I completely rely on it. But I don't do TDD the way the textbooks tell you to. Here is the pragmatic, AI-driven TDD workflow that actually works for me.


The Problem: AI Causes Reviewer Fatigue

When you first start using AI coding assistants, the speed is intoxicating. You write a comment, hit tab, and 50 lines of boilerplate appear. But within a few weeks, a dark reality sets in: AI turns you into a full-time code reviewer. Reading code is inherently harder than writing it. When an LLM blasts out complex logic, you have to sit there and trace every variable, look for off-by-one errors, and check for edge-case hallucinations. You spend so much cognitive energy verifying the AI's output that the speed gains completely evaporate. You stop being a builder and become a babysitter. And English is a terrible programming language to use for babysitting. It is ambiguous and subjective. You know what isn't ambiguous? A failing test.


The Workflow: Engineering the Edge Cases

I realised that writing the implementation and writing the tests still felt like doing the job twice. So, I stopped writing both. My job as a senior engineer isn't typing syntax; it is defining the boundaries of the system. Here is the exact loop I use now:

1. The Human Brain

I read the ticket. I don't touch the code yet. I spend my time doing the actual software engineering: working out the exact requirements, mapping the business logic, and actively hunting for edge cases. What happens if the payload is null? What if the user role is undefined? What if the date is in the past?

2. AI Generates the Tests (Red)

Instead of writing the tests myself, I feed those requirements and edge cases into the LLM. I tell it: Here are the business rules and the edge cases. Write a comprehensive suite of failing unit tests for this upcoming implementation. The AI generates all the tedious mocking, setup, and assertions. I run them. They fail. The boundary is set.

// Example of an AI-generated test suite for a user registration service
describe('UserRegistrationService', () => {
  it('should throw an error if the email is invalid', () => {
    const result = registerUser({ email: 'bad-email', password: 'password123' });
    expect(result.success).toBe(false);
    expect(result.error).toBe('Invalid email format');
  });
});

3. AI Generates the Implementation (Green)

Next, I point the AI at its own failing test suite and simply say: Write the implementation to make these tests pass. I don't have to explain the intricacies of the code; the assertions explain it for me. It writes the code, and the suite goes green.

4. The Human Refines

Now that I have a working implementation wrapped in a safety net of tests, I step back in. I review the code, ask the AI to refactor for readability, optimize a loop, or decouple a service. If the tests stay green, I know the logic holds.


The Crucial Step: The Git Stash Sanity Check

LLMs are notoriously lazy and sometimes sneaky. Occasionally, an AI will write a test that is functionally useless. To ensure I haven't been tricked by the LLM, I perform a mandatory sanity check before I commit:

  1. Stash: I run git stash to temporarily hide the implementation code, but I keep the test file active.
  2. Verify the Red: I run the test suite. It absolutely must fail. If it passes without the implementation code, the AI wrote a garbage test.
  3. Pop: I run git stash pop to bring the implementation back.
  4. Verify the Green: I run the test suite again. It passes.

Shifting the Cognitive Load

By adopting this specific brand of AI-assisted TDD, the division of labour is finally balanced. I define the 'What', and the AI executes the 'How'. If you are currently exhausted by constantly line-reading your AI assistant's subtle bugs, stop trying to write better English prompts. Do the systems thinking, make the AI write the failing tests, and let the compiler do the babysitting for you.

END OF DISPATCH