Table of Contents
- What is Test-Driven Development (TDD)?
- The TDD Workflow: Red-Green-Refactor
- Setting Up Your Environment
- Real-Time Example: Building a Calculator
- Conclusion
- References
What is Test-Driven Development (TDD)?
At its core, TDD is a cycle of three steps: Red, Green, and Refactor.
- Red: Write a test for a specific feature. Since the feature hasn’t been implemented yet, the test will fail (hence “Red”).
- Green: Write the minimal code required to make the test pass. Don’t over-engineer—just enough to get the test to “Green.”
- Refactor: Improve the code’s readability, performance, or structure without changing its behavior. Ensure all tests still pass after refactoring.
This cycle repeats for every new feature or bug fix, ensuring your code is always backed by tests.
The TDD Workflow: Red-Green-Refactor
Let’s break down the workflow with a visual:
-
Red Phase:
- Define a small, testable feature (e.g., “a calculator should add two numbers”).
- Write a test that verifies this feature.
- Run the test—it will fail (expected, since no code exists yet).
-
Green Phase:
- Write the simplest code possible to make the test pass.
- Run the test again—it should now pass (“Green”).
-
Refactor Phase:
- Clean up the code (e.g., remove duplication, improve naming, optimize).
- Re-run tests to ensure behavior hasn’t changed.
Setting Up Your Environment
To follow along, you’ll need:
- Python 3.6+ (installed from python.org).
- A code editor (e.g., VS Code, PyCharm).
Project Structure
Create a new directory for your project (e.g., tdd_calculator) with two files:
calculator.py: Where we’ll write the calculator logic.test_calculator.py: Where we’ll write our tests (using Python’s built-inunittestframework).
Real-Time Example: Building a Calculator
We’ll build a calculator with basic operations: addition, subtraction, multiplication, and division. We’ll follow TDD for each operation.
Step 1: Write the First Test (Red Phase)
Let’s start with addition. Our first test will verify that add(2, 3) returns 5.
In test_calculator.py, write:
import unittest
from calculator import add # We’ll implement `add` later
class TestCalculator(unittest.TestCase):
def test_add(self):
result = add(2, 3)
self.assertEqual(result, 5) # Expect 2 + 3 = 5
if __name__ == '__main__':
unittest.main()
Why this fails: We haven’t written the add function yet, so running the test will throw an ImportError.
Run the test:
python test_calculator.py
Output (simplified):
E
======================================================================
ERROR: test_add (test_calculator.TestCalculator)
----------------------------------------------------------------------
ImportError: cannot import name 'add' from 'calculator'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
This is the Red Phase—the test fails as expected.
Step 2: Write Minimal Code to Pass the Test (Green Phase)
Now, write the minimal code in calculator.py to make test_add pass:
def add(a, b):
return a + b
Run the test again:
python test_calculator.py
Output:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
The test passes! We’re in the Green Phase.
Step 3: Refactor (Refactor Phase)
Refactoring improves code quality without changing behavior. Our add function is already simple, so no changes are needed here. But if we had messy code (e.g., redundant variables), we’d clean it up now—and re-run tests to ensure they still pass.
Adding Subtraction
Let’s add subtraction. Repeat the TDD cycle.
Red Phase: Write the Test
In test_calculator.py, add a new test method:
def test_subtract(self):
result = subtract(5, 3) # New function: subtract
self.assertEqual(result, 2) # Expect 5 - 3 = 2
Run the test—it will fail (no subtract function exists).
Green Phase: Implement Subtraction
In calculator.py, add:
def subtract(a, b):
return a - b
Update the import in test_calculator.py to include subtract:
from calculator import add, subtract
Run the tests:
python test_calculator.py
Output:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Green phase complete!
Adding Multiplication
Next, multiplication.
Red Phase: Test
Add to TestCalculator:
def test_multiply(self):
result = multiply(4, 5) # New function: multiply
self.assertEqual(result, 20) # Expect 4 * 5 = 20
Green Phase: Implement
In calculator.py:
def multiply(a, b):
return a * b
Update the import in test_calculator.py:
from calculator import add, subtract, multiply
Tests pass—green phase done.
Adding Division (and Edge Cases)
Division is trickier because of edge cases (e.g., division by zero). Let’s handle normal division first, then the edge case.
Step 1: Test Normal Division (Red Phase)
Add to TestCalculator:
def test_divide(self):
result = divide(10, 2) # New function: divide
self.assertEqual(result, 5) # Expect 10 / 2 = 5
Step 2: Implement Division (Green Phase)
In calculator.py:
def divide(a, b):
return a / b
Update the import:
from calculator import add, subtract, multiply, divide
Tests pass.
Step 3: Test Division by Zero (Red Phase)
What if someone tries to divide by zero? We should raise a ValueError.
Add a test to TestCalculator:
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(5, 0) # Expect ValueError when dividing by 0
Run the test—it will fail because divide(5, 0) currently raises a ZeroDivisionError (Python’s default), not a ValueError.
Step 4: Fix Division by Zero (Green Phase)
Update divide in calculator.py to handle division by zero:
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Run the tests again—all pass!
Refactoring: From Functions to a Class
So far, we have separate functions (add, subtract, etc.). As we add more operations, a Calculator class with methods will be cleaner and more maintainable. This is a perfect opportunity to refactor!
Red Phase: Update Tests for a Class
Modify test_calculator.py to use a Calculator class:
import unittest
from calculator import Calculator # Now we’ll use a class
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator() # Create a Calculator instance for all tests
def test_add(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_subtract(self):
result = self.calc.subtract(5, 3)
self.assertEqual(result, 2)
def test_multiply(self):
result = self.calc.multiply(4, 5)
self.assertEqual(result, 20)
def test_divide(self):
result = self.calc.divide(10, 2)
self.assertEqual(result, 5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
self.calc.divide(5, 0)
if __name__ == '__main__':
unittest.main()
Run the tests—they’ll fail because Calculator doesn’t exist.
Green Phase: Implement the Class
Update calculator.py to a class:
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Run the tests—all pass!
Refactor Phase: Clean Up
Our class is already clean, but we could add docstrings for clarity (without changing behavior):
class Calculator:
"""A simple calculator with basic arithmetic operations."""
def add(self, a, b):
"""Return the sum of a and b."""
return a + b
def subtract(self, a, b):
"""Return the difference of a and b."""
return a - b
def multiply(self, a, b):
"""Return the product of a and b."""
return a * b
def divide(self, a, b):
"""Return the quotient of a divided by b.
Raises ValueError if b is zero.
"""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Tests still pass—refactoring complete!
Conclusion
In this example, we built a calculator using TDD. We started with a failing test, wrote minimal code to pass it, and refactored to improve design. TDD ensured every feature was tested, and refactoring kept the code clean.
Key takeaways:
- TDD forces you to think about requirements before writing code.
- Tests act as documentation and catch regressions when you refactor.
- The Red-Green-Refactor cycle keeps development focused and iterative.
Try TDD on your next project—start small, and you’ll quickly see the benefits!
References
- Python
unittestDocumentation - Beck, K. (2003). Test-Driven Development: By Example. Addison-Wesley.
- Martin Fowler’s TDD Article