
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.
Key takeaways
- Test-driven development writes the failing test first, and that ordering turns the test into the specification an AI coding assistant generates against.
- Reviewing tests is now a better use of senior engineering time than reviewing the implementations AI assistants generate.
- Negative testing catches what AI-generated code misses most reliably: empty inputs, null values, timeouts and permission errors.
- Testcontainers runs real databases and queues in CI, closing the gap that mocked dependencies leave open for generated code.
- Put automation and testing discipline in place before scaling AI assistants across a team, or defects simply move later in the cycle.
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.
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.
Frequently asked questions
Is test-driven development worth it?
Test-driven development pays off for teams shipping a high volume of code, especially teams using AI coding assistants. In an 18-month WeAreBrain engagement built around automated testing and continuous monitoring, production issues fell by 25%, release cycles shrank from months to weeks and team velocity rose by over 40%.
Does test-driven development slow down development?
Test-driven development adds thinking time up front, but AI assistants have cut the implementation time inside the red-green-refactor loop to seconds. The remaining cost is defining expected behaviour, which teams otherwise pay later as defects. Writing tests first moves that effort earlier in the cycle, where it is cheaper.
What is the difference between TDD and BDD?
Test-driven development writes developer-level tests before code, focused on how individual units behave. Behaviour-driven development, or BDD, describes expected behaviour in plain-language scenarios that business stakeholders can read, often in a given-when-then format. BDD builds on TDD, and teams frequently use both: BDD for features, TDD for implementation.
Can AI write unit tests?
AI assistants can write unit tests quickly, but generated tests can cover every line without asserting anything meaningful. WeAreBrain treats tests as the highest-value artefact a human writes, so senior engineers review the assertions. AI-generated unit tests become useful once a person has checked what each one actually verifies.
Is 100% code coverage enough?
Code coverage of 100% shows that every line ran during testing, but says nothing about whether the tests checked the right outcomes. AI assistants can produce fully covered suites with weak assertions, so coverage figures should always be read alongside what each assertion verifies, including negative cases for invalid input.



