Skip to main content

Test-Driven Development: Write the Test First, Then the Code

Advanced25 min5 exercises90 XP
0/5 exercises

Imagine you're drawing a picture. Most people sketch the outline first, then fill in the colors. You wouldn't start by painting random patches of blue and hope a sky appears.

Test-Driven Development (TDD) works the same way. Instead of writing code first and testing later, you write the test first — the "outline" of what your code should do — and then write just enough code to make that test pass.

It sounds backward, but TDD is used by developers at companies like Thoughtbot, Pivotal, and many open-source projects. In this tutorial, you'll learn the TDD workflow step by step and practice it yourself.

What Is the Red-Green-Refactor Cycle?

TDD follows a simple three-step cycle, repeated over and over:

  • RED — Write a test that fails (because the code doesn't exist yet)
  • GREEN — Write the simplest code that makes the test pass
  • REFACTOR — Clean up the code without changing what it does
  • Think of it like building with LEGO. You look at the picture on the box (RED — you know what you want). You snap the pieces together (GREEN — it works). Then you straighten everything up and remove extra pieces (REFACTOR — it's clean).

    The Red-Green-Refactor cycle in action
    Loading editor...

    The key discipline of TDD is that you never write production code without a failing test first. Every line of code exists because a test demanded it.

    How Do You Build FizzBuzz with TDD?

    FizzBuzz is a classic coding challenge: for numbers 1 to N, print "Fizz" if divisible by 3, "Buzz" if divisible by 5, "FizzBuzz" if divisible by both, or the number itself otherwise. Let's build it test by test.

    Cycle 1: Regular numbers. We start with the simplest case — a number that's not divisible by 3 or 5 should just return itself as a string.

    TDD Cycle 1 — Regular numbers
    Loading editor...

    Cycle 2: Fizz. Now we add a test for multiples of 3. The current code will fail (RED), so we add just enough logic to pass.

    TDD Cycle 2 — Adding Fizz
    Loading editor...

    Cycle 3: Buzz. Same pattern — write a failing test for multiples of 5, then make it pass.

    TDD Cycle 3 — Adding Buzz
    Loading editor...

    Cycle 4: FizzBuzz. The final case — multiples of both 3 and 5. This check must come first!

    TDD Cycle 4 — Complete FizzBuzz
    Loading editor...

    What Are the Benefits of Test-Driven Development?

    TDD gives you several superpowers:

  • Fewer bugs — you catch problems before they exist
  • Better design — writing tests first forces you to think about your function's interface before its implementation
  • Built-in documentation — your tests show exactly how the code should behave
  • Confidence to refactor — change anything, run the tests, know instantly if you broke something
  • Here's a subtle benefit: TDD prevents you from writing code you don't need. If there's no test for it, you don't write it. This keeps your codebase lean and focused.

    Without TDD (code-first)
    # Write a big function
    # Hope it works
    # Manually test a few cases
    # Ship it
    # Find bugs in production
    # Fix bugs under pressure
    With TDD (test-first)
    # Write a small test
    # Write minimal code to pass
    # Refactor if needed
    # Add next test
    # Repeat until done
    # Ship with confidence

    When Does TDD Work Best?

    TDD shines in certain situations:

  • Clear requirements — you know what the function should return for given inputs
  • Business logic — calculations, validations, data transformations
  • Bug fixes — write a test that reproduces the bug, then fix it
  • Library code — functions that others will depend on
  • TDD is harder (but still possible) when:

  • UI work — testing visual layouts is tricky
  • Exploring a new problem — you might not know the right interface yet
  • Integration code — connecting to real databases or APIs (use mocks!)
  • What Mistakes Should You Avoid with TDD?

    Beginners often make these TDD mistakes:

  • Writing too many tests at once — you should write ONE test, make it pass, then write the next
  • Writing too much code in the GREEN step — only write what's needed to pass the current test
  • Skipping the REFACTOR step — clean code is just as important as working code
  • Testing implementation instead of behavior — test WHAT the code does, not HOW it does it
  • Test behavior, not implementation
    Loading editor...

    Another common mistake is writing tests that are too tightly coupled. Each test should check one thing. If a test fails, you should know exactly what went wrong.

    Too many things in one test
    def test_everything():
        assert add(1, 2) == 3
        assert subtract(5, 3) == 2
        assert multiply(4, 3) == 12
        # If this fails, which function broke?
    One concern per test
    def test_add():
        assert add(1, 2) == 3
    
    def test_subtract():
        assert subtract(5, 3) == 2
    
    def test_multiply():
        assert multiply(4, 3) == 12

    Practice Exercises

    Your First TDD Cycle
    Write Code

    Use TDD to build an is_palindrome function. Write the test first, then the function. Your function should return True if a string reads the same forwards and backwards (case-insensitive), and False otherwise. Test with at least 3 cases: a palindrome, a non-palindrome, and an empty string. Print TDD palindrome test passed! at the end.

    Loading editor...
    TDD: Build a Password Validator
    Write Code

    Use TDD to build a validate_password function that returns a list of error messages. Rules: (1) must be at least 8 characters, (2) must contain at least one digit, (3) must contain at least one uppercase letter. Return an empty list if the password is valid. Write tests for: a valid password, a too-short password, one missing a digit, and one missing uppercase. Print each test result as PASS: <test_name>, then print Password validator complete!.

    Loading editor...
    Predict the TDD Process
    Predict Output

    This code simulates a TDD process with a test runner that reports RED or GREEN for each cycle. Read carefully and predict what gets printed.

    Loading editor...
    TDD Bug Fix: Reproduce Then Fix
    Write Code

    A user reported that count_vowels returns wrong results for uppercase strings. Use the TDD bug-fix approach: (1) write a test called test_uppercase_vowels that reproduces the bug with "HELLO" (should return 2), (2) fix the function, (3) add a test called test_mixed_case for "PyThOn" (should return 1), (4) make sure all tests pass. Print PASS: <test_name> for each test, then Bug fix verified!.

    Loading editor...
    TDD Challenge: Build a Word Counter
    Write Code

    Use TDD to build a word_count function that takes a string and returns a dictionary with each word (lowercased) as a key and its count as the value. Handle these cases: (1) normal sentence, (2) repeated words, (3) mixed case (treat "The" and "the" as the same), (4) empty string returns empty dict. Write a test for each case, run them all, and print PASS: <test_name> for each, then Word counter complete!.

    Loading editor...