Red-green-refactor — the TDD loop: fail → pass → clean
Concept
Red-Green-Refactor — the three-phase loop of TDD (Test-Driven Development). Each iteration drives a small increment of working, clean code.
RED: A test fails. The code doesn't implement the behavior yet (or a bug exists). The test is RED because most test runners display failing tests in red. This phase is important: a test that never fails doesn't prove anything.
GREEN: The code is written to make the test pass. The simplest, quickest implementation that makes the test go green. NOT clean code — just correct code. "Fake it till you make it" is valid here. Return a hardcoded value if that makes the test pass — you'll generalize it in subsequent cycles.
REFACTOR: With tests green (safety net in place), improve the code. Extract methods, rename things, remove duplication, improve the design. Run tests after each small refactoring step. If a test goes red, you broke something — undo the last change.
The loop size: One RED-GREEN-REFACTOR loop should be small — 5-15 minutes. Not a whole feature at once. One behavior at a time.
Why "fake it till you make it": In the GREEN phase, you want minimum code. If the test is assertEquals(5, sum(2, 3)), returning 5 hardcoded makes it green. When you add a second test assertEquals(10, sum(4, 6)), the hardcoded 5 fails — now you're forced to generalize.
The refactor phase is not optional: Many developers skip it ("I'll clean up later"). Skipping accumulates technical debt. The refactor phase IS where you design. The design emerges from refactoring clean code that already works.
Code Example
<?php
// RED-GREEN-REFACTOR demonstrated step by step:
// === CYCLE 1 ===
// RED: Write failing test
class PasswordValidatorTest extends \PHPUnit\Framework\TestCase
{
public function test_accepts_password_with_8_characters(): void
{
$validator = new PasswordValidator();
$this->assertTrue($validator->isValid('Pass1234')); // FAIL — class doesn't exist
}
}
// GREEN: Minimum to pass
class PasswordValidator
{
public function isValid(string $password): bool
{
return strlen($password) >= 8; // simplest implementation
}
}
// PASS ✓
// REFACTOR: Nothing to clean up yet — too simple
// === CYCLE 2 ===
// RED: Add another requirement
public function test_rejects_password_without_uppercase(): void
{
$validator = new PasswordValidator();
$this->assertFalse($validator->isValid('pass1234')); // FAIL — only checks length
}
// GREEN: Add the minimum
public function isValid(string $password): bool
{
return strlen($password) >= 8 && preg_match('/[A-Z]/', $password);
}
// PASS ✓
// === CYCLE 3 ===
// RED
public function test_rejects_password_without_number(): void
{
$this->assertFalse((new PasswordValidator())->isValid('Password')); // FAIL
}
// GREEN
public function isValid(string $password): bool
{
return strlen($password) >= 8
&& preg_match('/[A-Z]/', $password)
&& preg_match('/[0-9]/', $password);
}
// PASS ✓
// REFACTOR: Extract rules into named methods (tests still green after each step)
public function isValid(string $password): bool
{
return $this->hasMinimumLength($password)
&& $this->hasUppercase($password)
&& $this->hasNumber($password);
}
private function hasMinimumLength(string $password): bool { return strlen($password) >= 8; }
private function hasUppercase(string $password): bool { return (bool) preg_match('/[A-Z]/', $password); }
private function hasNumber(string $password): bool { return (bool) preg_match('/[0-9]/', $password); }
// Run tests → still PASS ✓ — refactor succeeded
// REFACTOR more — make rules configurable
public function isValid(string $password): bool
{
return collect($this->rules())->every(fn($rule) => $rule($password));
}
private function rules(): array
{
return [
fn($p) => strlen($p) >= 8,
fn($p) => (bool) preg_match('/[A-Z]/', $p),
fn($p) => (bool) preg_match('/[0-9]/', $p),
];
}
// Run tests → still PASS ✓ — design improved while all tests green