← Back to DevBytes

Test-Driven Development: Red-Green-Refactor Cycle

Test-Driven Development: The Red-Green-Refactor Cycle

Test-Driven Development (TDD) is a programming practice where you write tests before you write the production code that makes those tests pass. At the heart of TDD lies a simple but powerful three-step rhythm known as the Red-Green-Refactor cycle. This cycle, popularized by Kent Beck in the early 2000s, has become a cornerstone of modern software engineering because it produces cleaner code, fewer bugs, and a more confident development workflow.

What Is the Red-Green-Refactor Cycle?

The Red-Green-Refactor cycle describes the three phases you repeat for every piece of functionality you add to your system:

You repeat this loop many times per hour, building the system incrementally. Each cycle should be short — ideally a few minutes — so you get constant feedback on whether your code works.

Why TDD Matters

TDD flips the traditional development workflow on its head. Instead of writing code and then testing it afterward, you let the tests drive the design. This shift produces several important benefits:

How to Use the Red-Green-Refactor Cycle

Let us walk through a practical example using Python and the pytest framework. Suppose we want to build a small class that validates passwords according to a few rules: it must be at least 8 characters long, contain at least one uppercase letter, and contain at least one digit.

Step 1: Red — Write a Failing Test

We start by writing a test for the first rule: the password must be at least 8 characters long. We create a file called test_password_validator.py.

# test_password_validator.py
import pytest
from password_validator import PasswordValidator

def test_short_password_is_invalid():
    validator = PasswordValidator()
    result = validator.validate("Ab1")
    assert result.is_valid is False
    assert "Password must be at least 8 characters long" in result.errors

If we run this test now, it will fail because PasswordValidator does not exist yet. This is the Red phase — the test fails for the right reason.

$ pytest test_password_validator.py
# ImportError: cannot import name 'PasswordValidator' from 'password_validator'

Step 2: Green — Make It Pass

Now we write the simplest code that makes the test pass. We create password_validator.py.

# password_validator.py
class ValidationResult:
    def __init__(self, is_valid, errors):
        self.is_valid = is_valid
        self.errors = errors

class PasswordValidator:
    def validate(self, password):
        if len(password) < 8:
            return ValidationResult(False, ["Password must be at least 8 characters long"])
        return ValidationResult(True, [])

Running the test again should now pass. This is the Green phase.

$ pytest test_password_validator.py
# 1 passed

Step 3: Refactor — Improve the Code

Now we look at the code and tests for opportunities to improve. The code is small, but we might decide to extract the error message into a constant or simplify the result object. For now, the code is clean enough, so we move on to the next cycle.

Next Cycle: Add the Uppercase Rule

We return to Red and write a test for the uppercase requirement.

def test_password_without_uppercase_is_invalid():
    validator = PasswordValidator()
    result = validator.validate("abcdefg1")
    assert result.is_valid is False
    assert "Password must contain at least one uppercase letter" in result.errors

This test fails because our current implementation returns True for this input. We move to Green.

# password_validator.py
class PasswordValidator:
    MIN_LENGTH = 8

    def validate(self, password):
        errors = []
        if len(password) < self.MIN_LENGTH:
            errors.append("Password must be at least 8 characters long")
        if not any(c.isupper() for c in password):
            errors.append("Password must contain at least one uppercase letter")
        return ValidationResult(len(errors) == 0, errors)

Both tests now pass. We refactored the method to collect all errors into a list, which is cleaner than returning early. This is a natural Refactor step that emerged from adding the second rule.

Third Cycle: Add the Digit Rule

Back to Red with a new test.

def test_password_without_digit_is_invalid():
    validator = PasswordValidator()
    result = validator.validate("Abcdefgh")
    assert result.is_valid is False
    assert "Password must contain at least one digit" in result.errors

def test_valid_password_passes_all_rules():
    validator = PasswordValidator()
    result = validator.validate("Abcdefg1")
    assert result.is_valid is True
    assert result.errors == []

We add the digit check in Green.

class PasswordValidator:
    MIN_LENGTH = 8

    def validate(self, password):
        errors = []
        if len(password) < self.MIN_LENGTH:
            errors.append("Password must be at least 8 characters long")
        if not any(c.isupper() for c in password):
            errors.append("Password must contain at least one uppercase letter")
        if not any(c.isdigit() for c in password):
            errors.append("Password must contain at least one digit")
        return ValidationResult(len(errors) == 0, errors)

All tests pass. We could now Refactor by extracting each rule into its own small function or class, which would make the validator easier to extend in the future.

class PasswordValidator:
    MIN_LENGTH = 8

    def validate(self, password):
        rules = [
            self._check_length,
            self._check_uppercase,
            self._check_digit,
        ]
        errors = []
        for rule in rules:
            error = rule(password)
            if error:
                errors.append(error)
        return ValidationResult(len(errors) == 0, errors)

    def _check_length(self, password):
        if len(password) < self.MIN_LENGTH:
            return "Password must be at least 8 characters long"
        return None

    def _check_uppercase(self, password):
        if not any(c.isupper() for c in password):
            return "Password must contain at least one uppercase letter"
        return None

    def _check_digit(self, password):
        if not any(c.isdigit() for c in password):
            return "Password must contain at least one digit"
        return None

The tests still pass after this refactor, which confirms we did not break any behavior. This is the real power of TDD: the test suite gives you the courage to restructure code with confidence.

Best Practices for TDD

To get the most out of the Red-Green-Refactor cycle, keep these principles in mind:

Common Pitfalls to Avoid

Conclusion

The Red-Green-Refactor cycle is a deceptively simple discipline that transforms how you write software. By writing a failing test first, implementing just enough code to pass it, and then refining the design, you build systems incrementally with constant feedback. The result is a codebase that is well-tested, cleanly designed, and safe to evolve. Like any skill, TDD takes practice to internalize, but once the rhythm becomes second nature, you will find yourself delivering features faster and with far fewer defects than before.

— Ad —

Google AdSense will appear here after approval

← Back to all articles