Test-Driven Development
Red-Green-Refactor: write tests first to drive better design and catch regressions
Free tier: read the explanation here. Upgrade to Pro for Drills, Speed challenges & Mastery badges.
UpgradeKnowledge Debt detected
You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.
Explanation
Test-Driven Development (TDD) flips the usual order: you write the test *before* the implementation.
The Red-Green-Refactor cycle:
- 1Red — Write a test for behavior that doesn't exist yet. Run it; watch it fail (red).
- 2Green — Write the *minimum* code to make the test pass. Don't over-engineer.
- 3Refactor — Clean up the code while keeping tests green. Tests are your safety net.
# STEP 1 — Red: write a test for a function that doesn't exist yet
def test_fizzbuzz_three():
assert fizzbuzz(3) == "Fizz" # fails — fizzbuzz not defined
# STEP 2 — Green: write the minimum code
def fizzbuzz(n: int) -> str:
if n % 3 == 0:
return "Fizz"
return str(n)
# Test passes. Now add the next test:
def test_fizzbuzz_five():
assert fizzbuzz(5) == "Buzz" # fails — extend the implementation
# STEP 3 — Refactor after more tests are green
def fizzbuzz(n: int) -> str:
if n % 15 == 0: return "FizzBuzz"
if n % 3 == 0: return "Fizz"
if n % 5 == 0: return "Buzz"
return str(n)Why TDD produces better design:
- Forces you to use your own API before writing it — reveals awkward interfaces early
- Keeps functions small and focused (hard-to-test code is a design smell)
- Each new feature starts with a failing test → prevents the "I'll add tests later" trap
Regression safety — the key long-term payoff:
python # You have 47 tests. You refactor payment logic for speed. # Run pytest — still 47/47 green → you didn't break anything. # Without tests: you'd have to manually retest every feature path.
Test what, not how:
python # BAD — tests implementation detail (will break on refactor) def test_uses_hashmap_internally(): svc = CacheService() assert isinstance(svc._store, dict) # GOOD — tests observable behavior def test_cache_returns_stored_value(): svc = CacheService() svc.set("key", "value") assert svc.get("key") == "value"
Coverage is a floor, not a goal:
bash pytest --cov=src --cov-report=term-missing `` 100% coverage doesn't mean your tests are good — a test with no assertions can hit every line. Coverage tells you what's *untested*; it doesn't tell you if the tests are *meaningful*.
Examples
TDD cycle: building a password validator
Each cycle adds exactly one requirement; refactoring happens only while all tests stay green
# Iteration 1 — Red
def test_short_password_is_invalid():
assert is_valid_password("abc") is False # fails — function doesn't exist
# Green — minimum implementation
def is_valid_password(password: str) -> bool:
return len(password) >= 8
# Iteration 2 — Red (new requirement: must have a digit)
def test_no_digit_is_invalid():
assert is_valid_password("abcdefgh") is False # fails
# Green
def is_valid_password(password: str) -> bool:
return len(password) >= 8 and any(c.isdigit() for c in password)
# Iteration 3 — Red (must have uppercase)
def test_no_uppercase_is_invalid():
assert is_valid_password("abcdefg1") is False # fails
# Green + Refactor
def is_valid_password(password: str) -> bool:
return (
len(password) >= 8
and any(c.isdigit() for c in password)
and any(c.isupper() for c in password)
)Tests as regression guard during refactoring
The tests act as a contract: any implementation that satisfies them is correct, regardless of internal algorithm
# Before refactor: naive O(n²) duplicate-finder
def find_duplicates(items: list) -> list:
result = []
for i, x in enumerate(items):
for j, y in enumerate(items):
if i != j and x == y and x not in result:
result.append(x)
return result
# All tests pass:
def test_finds_duplicates():
assert find_duplicates([1, 2, 1, 3, 2]) == [1, 2]
def test_no_duplicates():
assert find_duplicates([1, 2, 3]) == []
def test_empty_list():
assert find_duplicates([]) == []
# Refactor to O(n) — run tests to confirm correctness is preserved
def find_duplicates(items: list) -> list:
seen, dupes = set(), []
for x in items:
if x in seen and x not in dupes:
dupes.append(x)
seen.add(x)
return dupes
# pytest: 3/3 passed — refactor is safeHow well did you understand this?