
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 layer | What it catches | What AI-generated code typically gets wrong |
|---|---|---|
| Unit tests | Logic errors in a single function | Unhandled edge cases and silent default values |
| Negative tests | Behaviour on invalid or missing input | Assumes valid input throughout |
| Integration tests with Testcontainers | Contract failures against real dependencies | Transaction boundaries, query behaviour, connection handling |
| End-to-end tests | Broken user journeys across services | Sequencing 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.



