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!.