Test-Driven Development: Write the Test First, Then the Code
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:
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 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.
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.
Cycle 3: Buzz. Same pattern, write a failing test for multiples of 5, then make it pass.
Cycle 4: FizzBuzz. The final case, multiples of both 3 and 5. This check must come first!
What Are the Benefits of Test-Driven Development?
TDD gives you several superpowers:
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.
# Write a big function
# Hope it works
# Manually test a few cases
# Ship it
# Find bugs in production
# Fix bugs under pressure# Write a small test
# Write minimal code to pass
# Refactor if needed
# Add next test
# Repeat until done
# Ship with confidenceWhen Does TDD Work Best?
TDD shines in certain situations:
TDD is harder (but still possible) when:
What Mistakes Should You Avoid with TDD?
Beginners often make these TDD mistakes:
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.
def test_everything():
assert add(1, 2) == 3
assert subtract(5, 3) == 2
assert multiply(4, 3) == 12
# If this fails, which function broke?def test_add():
assert add(1, 2) == 3
def test_subtract():
assert subtract(5, 3) == 2
def test_multiply():
assert multiply(4, 3) == 12Practice Exercises
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.
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!.
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.
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!.
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!.