What Is Test-Driven Development (TDD)?

Published:

by

Image for a blog about test-driven development

A pull request lands with every check green and a commit titled “add tests.” We open the commit history and see that we wrote each test after the implementation it covers, the reverse of the order test-driven development (TDD) prescribes.

Although our ticket asked a rate limiter to refuse the request that crosses the configured threshold, the code lets that request through and refuses the next one. The tests assert the off-by-one behavior anyway, since that is what the code did on the day we wrote them. Nothing in the pipeline can catch the mismatch, because the pipeline checks the code against tests we derived from that code, and the ticket that stated the intent never enters it.

The cost of that mismatch rises when an artificial intelligence (AI) coding agent writes both the code and the tests in the same pass. A test written first, from the ticket, would have failed on the request the code let through. A failing test states the requirement precisely enough that both the agent and the engineer reviewing its work can be held to it. This guide covers what TDD is, how its red-green-refactor cycle runs and compares with test-last, how to hold a coding agent to a test it cannot rewrite, where TDD sits beneath acceptance tests and specifications, which failure modes catch experienced teams, and how requirement-to-test traceability keeps a green test meaningful after its requirement changes.

What TDD Means

We write a small test for one behavior and watch it fail before adding the production code that makes it pass. TDD applies that sequence to each piece of production code. Kent Beck developed it in the late 1990s as part of Extreme Programming. The definition Ron Jeffries and Grigori Melnik published as guest editors of the Institute of Electrical and Electronics Engineers (IEEE) Software special issue on TDD (May/June 2007) still holds: “a discipline of design and programming where every line of new code is written in response to a test the programmer writes just before coding.”

The test written before the code is the most precise statement of a requirement: where a functional requirement says the limiter shall reject requests above a threshold, the test builds the request that crosses it and fails until the code rejects it. Because the test has to call the code before the code exists, writing it first settles the interface early, while the implementation behind it is still free to change. The tests themselves then become the artifact that coding agents are held to and that reviewers and auditors read.

Kent Beck’s canonical description of TDD starts one step earlier, with a desired change in behavior: list the expected variants in the new behavior, then turn exactly one item into a runnable test. Beck calls that list of variants the test list. Taking one item at a time off the list keeps each change small enough to watch fail.

Running the Red-Green-Refactor Cycle

The cycle keeps each change tied to observable behavior. Each of its three steps has one job, and the steps always run in this order:

  1. Red: The failing test from the definition starts the cycle and might not compile at first. Its failure identifies the behavior still to implement.
  2. Green: The minimum implementation makes the test work quickly. A hard-coded return value is acceptable here, because the step only has to reach a passing state, fast: for the rate limiter, refusing the exact request number the test sends is enough, and the next item on the test list forces the general comparison.
  3. Refactor: We eliminate duplication created while getting the test to work. Refactoring changes internal structure without changing observable behavior, so the tests stay green throughout.

How small should each cycle be?

Robert C. Martin’s three laws of TDD are the stricter version of the same rhythm: they permit production code only in response to a failing unit test and limit both test and production code to the minimum needed to fail or pass. The step size can be close to line by line, although the cycle can also use steps of varying size. A common misreading of the cycle is that TDD means writing every test before any code. Converting the whole test list into concrete tests up front creates speculative work that we must redo if making the first test pass changes an underlying design decision.

Comparing Test-First With Test-Last

A test written first shows the code does what the test demanded, while a test written after the code shows only that the code has not changed since the test recorded it. The ordering also changes how the suite responds to refactoring, and the table compares the two on what we care about when reviewing a pull request.

Test-first (TDD) Test-last
When the test is written Before the code, and it fails first After the code, and it passes first
What a green test proves The code does what the test demanded The code does what it did when the test was written
Source of the expected value The requirement or a worked example Often the implementation’s own output
Effect on refactoring Tests usually fixed to behavior, so structure can change Tests often fixed to structure, so changes break them
Record of intent at review The test predates the code and reads as a spec We read the code to learn the intent

How do outcomes differ between the two sequences?

The empirical record is stronger on quality than on productivity: in 2008 case studies of three Microsoft teams and one IBM team, pre-release defect density fell by 40% to 90% against similar projects that did not use TDD, while the teams reported a 15% to 35% increase in initial development time. A 2013 meta-analysis of 27 studies found a small positive effect on quality and little to no discernible effect on productivity. Its subgroup analysis found both the quality gain and the productivity cost much larger in industrial studies than in academic ones.

How Do We Keep the Failing Test Independent of the Agent?

We give the agent the test written in the red step and prohibit changes to it, so the agent has a fixed target that cannot shift to match its output. Left to sequence the work itself, a coding agent can invert the cycle by writing the implementation first, generating tests afterward, skipping confirmation of the red step, or writing code beyond the current test. Some agents go further by special-casing known test inputs, directly returning expected values or modifying tests to match the code’s output, all to get a passing build while disregarding the intended behavior.

In a feature-development workflow, we commit the test and its confirmed failure on the feature branch, then tell the agent to make all tests pass without modifying the tests. We back that instruction with the repository rather than the prompt. A branch protection rule that requires a code owner’s review on the test directories holds any change to a test file until a person approves it. Nothing counts as verified until we have reviewed the result and accepted it.

A useful separation of roles has us or a testing agent write the failing test and the coding agent implement against it, with a person reviewing the pull request at the end. The review confirms that no test was modified or disabled and that the implementation isn’t a lookup table of expected values. A failing continuous integration (CI) run that the agent answers with changes to test files alone is a red-alert condition.

A prompt, or a markdown file in the agent’s workspace, does not give reviewers and auditors lasting access to the requirement a test was written against. We store the link between the test and its requirement where both can reach it, and at that point keeping an agent honest becomes a question of traceability between requirements and tests. For a team introducing agents, that stored link also supports a measure that survives debates over developer velocity: how many merged changes were preceded by a test someone watched fail. Keeping the requirement identifier and the test’s red and green results in the review record turns that count into evidence a reviewer can check.

Positioning TDD Beneath Acceptance Tests and Specifications

TDD’s unit-level cycle runs inside a larger loop. An outer loop of acceptance tests guides each feature, and an inner loop of unit tests shapes the structure of the code, the multi-level approach Steve Freeman and Nat Pryce describe in Growing Object-Oriented Software, Guided by Tests. An acceptance test can stay red across several check-ins, whereas a failing unit test should not reach the shared main line. In acceptance test-driven development (ATDD), team members with customer, development and testing perspectives write that outer test together before anyone builds the feature. Behavior-driven development (BDD) carries the same test-first habit up to product behavior, written as examples the whole team can read.

Spec-driven development (SDD) sits above both loops for teams using coding agents. TDD pins down one unit’s behavior before that unit’s code exists, while SDD works at the feature and system level, where a structured specification defines behavioral contracts before any tests or code are written. Together the layers form a chain, and a TDD test with no requirement or acceptance test above it is precise about behavior and silent about why the behavior was wanted.

Avoiding TDD Failure Modes on Experienced Teams

One review question catches all five of the failure modes below: can this test fail for the intended reason, and is there evidence from the red step? Most TDD failures leave the suite green, so we can run the cycle for years with tests that break on every refactor or pass no matter what the code does. We’ve all written at least one test that could never fail, and five patterns come up repeatedly:

  • Over-mocking: A test that mocks every collaborator records how the method does its work, so a refactor that keeps the behavior intact is much more likely to break the test.
  • Testing implementation instead of behavior: A test written because a class gained a method binds to the class’s structure when no new behavior sits behind it. Verifying which functions the class calls turns the test into a change detector that fails whenever that structure moves.
  • Skipping the refactor step: Neglecting the third step is the most common way Martin Fowler hears of TDD going wrong, because the code ends up as a pile of fragments with tests but no clean design.
  • Tautological tests: A test that computes its expected value by calling the implementation passes by construction. Expected values have to come from somewhere else: a known-good literal, a worked example, the spec.
  • Test-after dressed as TDD: Most teams settle for test-after tests and never get to TDD itself, as Gil Zilberfeld observed in a 2016 InfoQ interview. The design feedback from writing the test first never happened, even though the tests exist and the coverage looks the same.

Tests that pass by construction fail the first half of the review question outright. Tests bound to internal structure fail it later, the first time a refactor turns them red while the behavior is unchanged.

The link between a requirement and its test has to hold long after the code ships. DO-178C requires it outright, and each requirement change is a chance for the linked test to stay green while its meaning goes stale.

What the Standards Require

In regulated software, a unit test that can’t be traced to a requirement carries no weight as compliance evidence. DO-178C requires bidirectional traceability between requirements at every level and source code. DO-178C also requires links from software requirements to test cases, procedures and results.

Automotive and railway programs answer to their own software development standards. Automotive software falls under Part 6 of the International Organization for Standardization (ISO) standard ISO 26262. ISO 26262-6:2018, titled “Product development at the software level,” covers software architecture, unit design, implementation and verification. Railway software falls under EN 50716:2023, which supersedes EN 50128:2011.

One constraint can cut against classical TDD: DO-178C verification independence means low-level requirement (LLR) test cases cannot be developed by the person who coded those requirements, wherever an objective must be met with independence. TDD’s inner loop has one engineer writing both, so where that independence applies, a different engineer writes the test cases against the same requirement. In that setting, TDD is still a design and defect-prevention practice, but it does not supply the independent verification and validation the program needs.

When the Requirement Changes

Once the linked requirement changes, the test keeps passing against the old intent, and a CI run has no way to know. Karl Wiegers, principal consultant at Process Impact, described the tool-level answer years ago: some tools “automatically flag a link as suspect whenever the object on either end of the link is modified.” We then check whether the linked item still holds, and checking that link by hand means re-reading every document that references the changed requirement.

Jama Connect®, a web-based requirements management and traceability platform for complex, regulated product development, holds the requirement and the test cases that verify it as linked items. When a linked requirement changes, suspect links mark every downstream test case for reassessment. A Global Standards Manager at Loram Maintenance of Way, Inc. describes how a change used to be checked: “it was a very manual process when using Word and Excel to make sure there were no errors in documents covering any references downstream from the changed requirement or upstream in the top-level requirements.” Loram, which submitted EN 50128 documentation to an independent assessment body at every project gate, reports an up to 24% reduction in software engineering hours.

The manual work that shrinks is the generation of the trace matrix itself, which is what Vave Health’s Head of Quality and Regulatory describes: “Jama Connect’s automation allows us to efficiently manage each release. The tool allows us to reduce manual processes involved in generating traceability and focus our efforts on reviews of the trace matrix.” The trace-matrix review is the same check an engineer runs when a requirement changes: read the test, decide whether it still says what the requirement means, and turn it red when the two have drifted apart.

How Jama Connect Supports TDD

Test cases in Jama Connect®’s built-in Test Center trace to the requirements they verify. The Traceability Information Model (TIM) can therefore flag a requirement with no test behind it, and a suspect link marks the test for review when that requirement changes. Under the Live Traceability™ approach, the same links run across requirements, design, tests and verification, and the open application programming interface (API) supports custom integrations with CI and continuous delivery (CD) pipelines. With the TIM at its center defining how requirements, models, code, tests, simulations and verification results relate, Jama Connect is the Product Context Layer for engineering teams, the governed system of record that an agent and the person approving its pull request both read from.

The Model Context Protocol (MCP) Server add-on, released with Jama Connect 9.35 on 4 May 2026 for Cloud and Self-Hosted deployments, works with external AI agents and tools built on large language models, so an agent can retrieve requirement and traceability data from Jama Connect, including the requirement behind the test it is working against. The server enforces existing permissions, lifecycle workflows and audit requirements. Jama Intelligence, an add-on for Jama Connect Cloud, drafts test cases from a requirement for an engineer to review; the trace link is created only when the engineer accepts a draft, and the engineer can regenerate drafts that don’t fit. Anything performed with AI inside Jama Connect is versioned and documented as generated by AI, so an audit can show what was generated, when and against which requirement. A test drafted by an external tool and saved as a file carries no such record.

Building Test-Driven Development Into Your Program

Writing the requirement identifier beside each item on the test list gives every later test a parent, and that record holds up only while someone notices when the requirement under a green test moves. Linking one requirement to the tests that verify it during a free 30-day trial period shows one of those tests turning suspect the moment the requirement changes.

Frequently Asked Questions About Test-Driven Development

Does TDD replace quality assurance or integration testing?

No, TDD covers the unit level and is written by the developer, and quality assurance (QA) keeps its separate role. When we treat TDD as a way to eliminate QA, we usually create needless tension around the practice. On regulated programs, standards such as DO-178C require requirements-based testing, traceability that connects LLRs to test cases and source code, and independent review or verification activities for some objectives.

Can TDD be introduced on legacy code?

Yes, and it can loosen the coupling that makes legacy code hard to change. High coupling is common in legacy code whose components were not designed to be tested in isolation. Michael Feathers’ Working Effectively with Legacy Code gives the isolation and abstraction techniques for getting a first test around code that has none.

What code coverage should a TDD team aim for?

Each program sets its own code-coverage target, and a TDD team should treat it as a floor, because coverage is a byproduct of the cycle. The evidence that matters is a test someone watched fail before the code made it pass. Tests without assertions raise the number without proving anything, so a figure reached that way is worth less than a lower honest one. The requirement side of the question sits in the TIM for requirements, which defines the expected development workflow for requirements and their tests.

YOU MAY ALSO LIKE

See Jama Connect in Action!

Our Jama Connect experts are ready to guide you through a personalized demo for requirements management for medical device development, answer your questions, and show you how
Jama Connect can help you identify risks, improve cross-team collaboration, and drive faster time to market through AI-native engineering management.