AtomLearnAtomLearn
DashboardGoalsPathAchievementsReviewSign In
DashboardTestingTest-Driven Development
TestingNot Started

Test-Driven Development

Red-Green-Refactor: write tests first to drive better design and catch regressions

0%
Knowledge0%
Learn & Drill
Fluency0%
Drill & Speed
Retention0%
Mastery & Review
Confidence0%
All modes
Practice

Free tier: read the explanation here. Upgrade to Pro for Drills, Speed challenges & Mastery badges.

Upgrade
Knowledge
Fluency
Retention

Knowledge 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:

  1. 1Red — Write a test for behavior that doesn't exist yet. Run it; watch it fail (red).
  2. 2Green — Write the *minimum* code to make the test pass. Don't over-engineer.
  3. 3Refactor — Clean up the code while keeping tests green. Tests are your safety net.
python
# 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 safe

How well did you understand this?