Answer
Yes, use TDD for business rules and calculations whose correct result you can state before you write the code: write one failing test from the requirement, write the code that makes it pass, refactor, and keep the tests in CI.
Verdict on the code under testYes
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Yes, use TDD on code the framework says to test. My typical case is a developer adding a business rule to a web backend, such as the date a support ticket escalates. Blast radius is users and Change frequency is regularly, because customers see the result and most features touch a rule. Detectability is eventually, because a wrong date looks plausible and raises no error. Reversibility is with-effort, since stored results need a repair script, and Test cost is moderate, about an hour per behaviour, so rule R11 gives Test. Writing the test before the code costs the same hour and takes the expected value from the requirement instead of the code's output.
| When | Decision | Why |
|---|---|---|
| The code is a spike to try a library, which only you run and then delete | Do not test the spike; start TDD when you rebuild the code for production | Blast radius falls to none, because only you run the spike and bear a failure |
| The rule calculates charges, discounts or invoice totals | Test mandatory: write a failing test for each amount and boundary before the code, and have a second person review the tests | Blast radius rises to money and Reversibility to costly, because a wrong charge ends in refunds |
| The rule decides which users may read a record | Test mandatory: write allowed, denied and cross-account tests before the code | Blast radius rises to safety-or-legal and Detectability to never, because a leaked record raises no error |
| The change is a colour, a margin or a label that the developer sees on screen right after the change | Do not test: skip TDD and look at the screen after the change | Detectability moves to immediately and Reversibility to trivial, because a revert removes a wrong label |
| Nobody can state the correct output in advance, such as a search ranking tuned by feel | Test it differently: write no test first; track clicks on the top results and alert when they drop | Test cost rises to heavy, because a useful test needs queries with judged results, while Detectability stays eventually |
| The code is a rule that archives idle projects, which changes a few times a year | Test minimally: one test written first for the main path, and a failing test before each bug fix | Change frequency falls to rarely, so fewer changes can break the rule |
What breaks if you don't test
A test written after the code often takes its expected value from what the code returned. If the code has a bug, the test records the bug as correct, CI stays green, and customers find the wrong result weeks later. A test written first has failed at least once, so you know it can catch the missing behaviour.
What you lose if you over-test
TDD on every class tends to produce one test per method, with a mock for each collaborator. A refactor that moves a method between classes then breaks dozens of tests while behaviour stays the same. In a Microsoft Research study of three Microsoft teams and one IBM team, TDD projects took 15 to 35 percent longer to build, a price that layout tweaks and throwaway spikes do not repay.
How to test
- List the cases from the requirement before any code, as Martin Fowler describes.
- Pick one case, write a test for it, run it, and read the failure message.
- Write the least code that passes, then refactor while the tests stay green.
- Test rules through the public method of the service at the unit level, and queries with an integration test against a real database.
- Before each bug fix, write the test that reproduces the bug and watch it fail.
Procedure and references
When the answer changes
- The code moves money or decides who may see a record.
- Nobody knows the right output until they see it, as with prototypes and visual design.
- Your team requires test-first on every change: follow the team rule.
Real incident + Code example
The due date the test agreed with
On a helpdesk product I worked on, dueDate computed when a ticket escalated to a manager. The developer wrote the function, ran it, and pasted its output into the assertion. The function counted the opening day as a working day, so tickets escalated one day early. The test stayed green for three weeks, until a customer's support lead asked why managers got alerts for tickets still on time. We fixed the rule and ran a script over every open ticket. A test written first from the rule's text fails on the buggy code:
import { expect, it } from "vitest";
import { dueDate } from "./sla";
// Written after the code: the expected value came from its output
it("computes the due date", () => {
expect(dueDate("2026-09-11", 2)).toBe("2026-09-14"); // Monday, the bug
});
// Written first, from the rule: the opening day does not count
it("gives a ticket opened on Friday with 2 working days until Tuesday", () => {
expect(dueDate("2026-09-11", 2)).toBe("2026-09-15");
});
Related questions
FAQ
- Is TDD worth it?
TDD is worth it for rules and calculations whose correct result you can state before writing them. In a Microsoft Research study of four industrial teams, TDD teams had 60 to 90 percent lower defect density than comparable teams without TDD.
- Should I start TDD on a project that has no tests?
Yes, start TDD on a project with no tests for each new rule and each bug fix, without backfilling tests for code nobody changes. Before changing old code, record what it returns today with characterization tests, then write the failing test for the new behaviour.
- Does it matter whether I write the test first or last?
The order of test and code matters less than the size of the steps, according to Fucci and colleagues (2016): short, steady cycles of test and code were linked to better quality, and test order had no important effect. Writing the test first still keeps expected values from being copied out of the code's output.