Test-Driven Development (TDD)
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). There are no detection_hints tools listed for this term. TDD is a practice/discipline, not a code construct — violation (writing tests after code, skipping refactor, testing implementation details) is only discoverable through code review, commit history inspection, or observing that test coverage was added after production code. No linter or static tool can reliably detect 'tests were not written first.'
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes starting a new feature with a failing PHPUnit test — straightforward for greenfield work. However, the common_mistakes indicate that if TDD has been abandoned (tests written after, implementation-coupled tests, slow test suites), correcting the practice requires retrofitting tests across multiple components, decoupling database/HTTP calls, and restructuring test assertions — spanning multiple files and a meaningful refactor effort.
Closest to 'persistent productivity tax' (b5). TDD applies to both web and cli contexts broadly. When adopted, every future maintainer must understand and follow the red-green-refactor cycle; slow test suites or poorly structured tests impose an ongoing tax on many work streams. The discipline shapes how every new feature and bug fix is approached, but it doesn't fully define the system's architecture — it's a persistent process overhead rather than a structural constraint.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field directly states the canonical wrong belief: TDD means writing ALL tests before ANY production code, when in reality it is a micro-cycle applied feature by feature. This is a well-documented, widely held misconception that most developers encounter and eventually correct, but it is not so severe as to contradict a parallel concept in another ecosystem — it's a scope misunderstanding rather than a behavioral inversion.
Also Known As
TL;DR
Explanation
TDD follows a three-step cycle: Red — write a test for the desired behaviour that fails because the implementation does not exist yet; Green — write the minimum code needed to make the test pass (no more); Refactor — improve the implementation while keeping all tests green. This cycle runs in minutes, not hours. TDD benefits: forces design thinking before coding (if a class is hard to test, it is poorly designed); produces a complete test suite as a byproduct; enables confident refactoring; provides executable documentation of expected behaviour. In PHP, PHPUnit is the standard test framework; Pest provides a more expressive syntax. TDD does not mean writing every line of code test-first — it means starting with tests for behaviour, especially for business logic, edge cases, and error conditions. UI and integration tests are often written after because they are slower and harder to write first.
Common Misconception
Why It Matters
Common Mistakes
- Writing tests after all the code is complete — this is testing, not TDD; the design benefits only come from writing tests first.
- Writing tests that test implementation details rather than behaviour — test what the method does, not how it does it.
- Skipping the Refactor step — Green does not mean done; code written to pass tests fast is often messy and must be cleaned up.
- Not running tests in under one second — if the test suite is slow, developers stop running it; keep unit tests fast by avoiding database and HTTP calls.
Avoid When
- Exploratory or spike code where the design is unknown — write tests after the spike, before productionising.
- Simple getter/setter or pure configuration code where tests add noise without catching meaningful bugs.
- Legacy code with no test infrastructure — retrofitting TDD into an untestable codebase requires refactoring first.
- Deadline pressure that makes writing tests first slower than the value it provides in that specific context.
When To Use
- Complex business logic with many edge cases — tests drive out the cases you would otherwise miss.
- APIs and interfaces that must be stable — writing the test first defines the contract before implementation.
- Bug fixes — write a failing test that reproduces the bug, then fix it; the test prevents regression.
- Refactoring — a passing test suite gives you the confidence to change internals without breaking behaviour.
Code Examples
// Code written first, tested after — gaps in coverage
class PriceCalculator {
public function calculate(int $qty, float $price, float $discount): float {
return ($qty * $price) * (1 - $discount / 100);
}
}
// Tests written after often miss edge cases:
// What if qty is 0? discount > 100? price is negative?
// TDD — test written first reveals edge cases before coding
class PriceCalculatorTest extends TestCase {
public function test_calculates_discounted_price(): void {
$calc = new PriceCalculator();
$this->assertEquals(90.0, $calc->calculate(1, 100.0, 10));
}
public function test_zero_quantity_returns_zero(): void {
$this->assertEquals(0.0, (new PriceCalculator())->calculate(0, 100.0, 0));
}
public function test_rejects_discount_over_100(): void {
$this->expectException(InvalidArgumentException::class);
(new PriceCalculator())->calculate(1, 100.0, 110);
// This test FAILS first — now implement the validation
}
}