TDD — Test-Driven Development: write the test before the code
Concept
TDD (Test-Driven Development) — a software development practice where you write the test BEFORE writing the implementation. The tests drive the design.
The TDD cycle: RED → GREEN → REFACTOR. Also called "Red-Green-Refactor."
Step by step:
- RED: Write a failing test for behavior that doesn't exist yet. Run tests — see it fail (red). This proves the test is actually testing something.
- GREEN: Write the MINIMUM code needed to make the test pass. Don't over-engineer — just make it pass.
- REFACTOR: Clean up the code (remove duplication, improve names, extract methods) while keeping tests green.
Why write the test first:
- Forces you to think about the interface BEFORE the implementation. "What should this method do from the caller's perspective?"
- Ensures every piece of code is tested (you can't forget to test what you just wrote).
- Makes tests the specification. Tests describe WHAT the code does.
- "If you can't write a test for it, you don't understand the requirement."
TDD vs "test after": Traditional: write code, then test it. TDD: test first, then code. In "test after" mode, tests often verify the implementation rather than the desired behavior.
Unit TDD vs BDD: TDD traditionally means unit-level. BDD (Behavior-Driven Development) applies the same "test first" idea at the feature level using natural-language test descriptions.
Real-world TDD: Many developers use TDD selectively — for complex logic, for new algorithms, for bug fixes (write a failing test that reproduces the bug, then fix it). Few practice strict TDD 100% of the time.
Code Example
<?php
// TDD CYCLE EXAMPLE: Building an Order calculation class
// STEP 1: RED — write a failing test first
class OrderCalculatorTest extends \PHPUnit\Framework\TestCase
{
public function test_calculates_subtotal_from_items(): void
{
$calculator = new OrderCalculator(); // class doesn't exist yet!
$items = [
['price' => 10.00, 'quantity' => 2], // 20.00
['price' => 5.00, 'quantity' => 3], // 15.00
];
$this->assertEquals(35.00, $calculator->subtotal($items)); // FAILS — class doesn't exist
}
}
// Run: php artisan test → FAIL (red): Class OrderCalculator not found
// STEP 2: GREEN — write MINIMUM code to pass
class OrderCalculator
{
public function subtotal(array $items): float
{
return array_sum(array_map(fn($i) => $i['price'] * $i['quantity'], $items));
}
}
// Run: php artisan test → PASS (green)
// STEP 3: REFACTOR — clean up while tests stay green
class OrderCalculator
{
public function subtotal(array $items): float
{
return array_reduce($items, fn($carry, $item) => $carry + $this->lineTotal($item), 0.0);
}
private function lineTotal(array $item): float
{
return $item['price'] * $item['quantity'];
}
}
// Run: php artisan test → still PASS (green) — refactoring successful
// Next cycle: RED — add tax calculation
public function test_calculates_total_with_tax(): void
{
$calculator = new OrderCalculator();
$items = [['price' => 100.00, 'quantity' => 1]];
$this->assertEquals(110.00, $calculator->total($items, taxRate: 0.10)); // new method
}
// FAIL — total() doesn't exist yet
// GREEN
public function total(array $items, float $taxRate): float
{
$subtotal = $this->subtotal($items);
return $subtotal + ($subtotal * $taxRate);
}
// PASS
// TDD for BUG FIXES — reproduce bug with a failing test, then fix
// Bug report: "A 0% discount crashes the app"
public function test_zero_discount_returns_full_price(): void
{
$this->assertEquals(100.00, (new OrderCalculator())->applyDiscount(100.00, 0));
// FAIL first (if bug exists), then fix, then PASS
}