0

TDD — Test-Driven Development: write the test before the code

Intermediate5 min read·eng-17-011
interview

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:

  1. 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.
  2. GREEN: Write the MINIMUM code needed to make the test pass. Don't over-engineer — just make it pass.
  3. 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
<?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
}