Skip to main content
Editor

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.

    # Step 1: Write the tests FIRST
    def test_is_palindrome():
        # Test a palindrome
        # Test a non-palindrome
        # Test empty string
        pass
    
    # Step 2: Write the function to make tests pass
    def is_palindrome(s):
        pass
    
    # Step 3: Run tests
    test_is_palindrome()
    print('TDD palindrome test passed!')
    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!.

    # Step 1: Write tests first
    # Step 2: Build validate_password to pass them
    # Step 3: Run and print results
    
    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.

    def run_tdd_cycle(name, test_fn, impl_fn=None):
        if impl_fn:
            impl_fn()
        try:
            test_fn()
            print(f'GREEN: {name}')
        except Exception:
            print(f'RED: {name}')
    
    functions = {}
    
    def test_celsius():
        assert functions['to_celsius'](32) == 0.0
    
    def test_boiling():
        assert functions['to_celsius'](212) == 100.0
    
    # Cycle 1: RED (function doesn't exist yet)
    run_tdd_cycle('celsius_basic', test_celsius)
    
    # Cycle 1: GREEN (add the function)
    def add_celsius():
        functions['to_celsius'] = lambda f: (f - 32) * 5 / 9
    
    run_tdd_cycle('celsius_basic', test_celsius, add_celsius)
    
    # Cycle 2: Does boiling point work too?
    run_tdd_cycle('celsius_boiling', test_boiling)
    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!.

    def count_vowels(text):
        count = 0
        for char in text:
            if char in 'aeiou':  # Bug is here!
                count += 1
        return count
    
    # Step 1: Write a test that REPRODUCES the bug
    
    # Step 2: Fix the function
    
    # Step 3: Add more tests
    
    # Step 4: Run all tests
    
    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!.

    # Use TDD: write tests first, then implement
    
    # Test 1: Normal sentence
    # Test 2: Repeated words
    # Test 3: Mixed case
    # Test 4: Empty string
    
    # Implement word_count
    
    # Run all tests
    

    Related Tutorials

    Was this tutorial helpful?