Test-driven development in the age of AI coding tools

Date
September 7, 2026
Hot topics 🔥
AI & Tech
Contributor
Dmitry Ermakov
Summarize with AI:
Person testing software illustration

Test-driven development is the practice of writing a failing test before writing the code that satisfies it. This article explains why that discipline becomes more valuable when a large share of your code is generated by AI assistants, what negative testing catches that generated code routinely misses, and how we run integration tests against real dependencies. It is written for engineering leads deciding how to keep quality steady while output volume rises.

Our teams generate far more code than we did two years ago. Review capacity has not grown to match. That gap is the actual quality risk in AI-assisted delivery, and it does not get solved by reviewing harder.

Test-driven development gives us a way through it. The tests become the specification the assistant works against, which turns a vague prompt into a precise contract and gives the reviewer something objective to check against.

What is test-driven development?

Test-driven development is a cycle of three steps: write a test that fails, write the smallest amount of code that makes it pass, then refactor while the test stays green. The discipline sits in the ordering, because writing the test first forces you to define the expected behaviour before you decide how to implement it.

The practice predates AI coding assistants by two decades. Kent Beck’s original argument was about design quality: tests written first produce simpler interfaces because you experience the interface as a caller before you build it. That argument still holds. What has changed is who writes the implementation.

What changes when AI writes the code

The tests stop being a safety net and become the brief. When an assistant generates the implementation, whatever you specified in the test is what you get, and whatever you left unspecified is filled in by inference.

We work with GitHub Copilot across our delivery teams, and the pattern is consistent. Given a well-formed failing test, the generated implementation is usually correct and fast. Given a loose prompt and no test, it produces something plausible that passes a casual read and fails on the third edge case in production.

Three practical consequences follow:

  • The test is the highest-value artefact a human writes. Reviewing tests is a better use of senior engineering time than reviewing implementations.
  • Speed moves the constraint. Generating an implementation takes seconds, so the time cost of the red-green-refactor loop is now dominated by the thinking, which is where it should be.
  • Coverage stops being a proxy for quality. An assistant can generate tests that cover every line without asserting anything meaningful. Coverage numbers need to be read alongside what the assertions actually check.

What is negative testing, and why generated code needs it

Negative testing verifies that a system behaves correctly when it receives invalid input, missing data or conditions it was not designed for. It matters more with generated code because assistants are trained to produce working solutions, so they optimise for the path where everything goes right.

The failures we see in review cluster in the same places: empty collections, null values arriving from an upstream system, timeouts on an external call, permission errors, and concurrent writes. Each one is obvious once named and almost never present in a first-pass generated implementation.

Our rule is that every generated function gets at least one test for what happens when its inputs are wrong. The OWASP testing guidance is a useful checklist for the security-relevant subset of those cases.

How we test integrations with Testcontainers

Testcontainers runs your real dependencies, such as databases, message queues and caches, inside disposable containers during the test run. We use it because mocked dependencies hide exactly the class of defect that generated code tends to introduce.

An assistant writing a database query will produce something that satisfies a mocked repository while getting the transaction boundary, the index behaviour or the null handling wrong against the real engine. Testcontainers removes that gap by running the actual database in CI, with a clean instance per test run.

Test layerWhat it catchesWhat AI-generated code typically gets wrong
Unit testsLogic errors in a single functionUnhandled edge cases and silent default values
Negative testsBehaviour on invalid or missing inputAssumes valid input throughout
Integration tests with TestcontainersContract failures against real dependenciesTransaction boundaries, query behaviour, connection handling
End-to-end testsBroken user journeys across servicesSequencing and state carried between services

What this looked like in practice

Over an 18-month engagement with a rapidly growing SaaS and freelance employee marketplace provider working in regulated sectors such as childcare and healthcare, we rebuilt the delivery approach around DASA DevOps principles, with automated testing and continuous monitoring at the centre alongside GitHub Copilot in the development workflow.

The measurable outcomes: a 25% reduction in production issues attributed to improved testing and continuous monitoring, release cycles shortened from months to weeks, team velocity up by over 40%, and infrastructure costs down 10%. The client also reached GDPR compliance and launched two new verticals during the same period.

The point worth taking from that project is the ordering. The automation investment came first, and the throughput gains followed. Teams that adopt AI assistants before the testing discipline is in place tend to move their defects later in the cycle rather than removing them. The DASA principles we followed put automation and continuous improvement ahead of tooling for exactly that reason.

Key takeaways

  • Test-driven development writes the failing test first, and that ordering turns the test into the specification an AI assistant generates against.
  • Reviewing tests is now a better use of senior engineering time than reviewing generated implementations.
  • Negative testing catches the failure modes AI-generated code misses most reliably: empty inputs, nulls, timeouts and permission errors.
  • Testcontainers runs real databases and queues in CI, closing the gap that mocked dependencies leave open.
  • Automation and testing discipline should be in place before AI assistants are scaled across a team, or defects simply move later in the cycle.

Want faster delivery without the defect tail?

We help engineering teams put the testing and automation foundations in place that make AI-assisted development safe to scale. If your output has risen faster than your confidence in it, talk to our engineering team.

SaveSaved
Summarize with AI:

Dmitry Ermakov

Dmitry is our our Head of Engineering. He's been with WeAreBrain since the inception of the company, bringing solid experience in software development as well as project management.
Woman holding the Working machines book

Working Machines

An executive’s guide to AI and Intelligent Automation

Working Machines eBook