| name | Test-Driven Development |
| description | Development methodology where tests drive design through short red-green-refactor cycles |
| category | software-development |
Test-Driven Development
What I do
I guide developers through a disciplined approach where tests are written before production code. TDD follows a simple cycle: write a failing test (Red), write minimal code to pass the test (Green), then refactor to improve design (Refactor). This methodology produces well-tested, loosely coupled, highly cohesive code from the start. TDD is not about testing—it's about design that is testable and therefore maintainable.
When to use me
Use TDD when building new features or modules, especially in critical systems where correctness matters. TDD excels when requirements are clear and stable, when you're working on complex business logic, or when you want to document expected behavior through tests. It's less suitable for exploratory coding, UI layout, or when dealing with legacy code that wasn't designed for testing.
Core Concepts
- Red-Green-Refactor: The core TDD cycle
- F.I.R.S.T. Tests: Fast, Isolated, Repeatable, Self-validating, Timely
- Test Pyramid: Many unit tests, fewer integration tests, few E2E tests
- Arrange-Act-Assert: Standard test structure
- Mocking: Isolating units under test
- Dependency Injection: Making code testable
- Code Coverage: Metric for test thoroughness
- Test Doubles: Dummies, Stubs, Mocks, Spies, Fakes
- Given-When-Then: BDD-style test structure
- Integration Points: Testing how components work together
Code Examples
Basic TDD Cycle
import unittest
from unittest.mock import patch
class TestShoppingCart(unittest.TestCase):
def test_add_item_increases_count(self):
cart = ShoppingCart()
cart.add_item("Apple", 1.00)
self.assertEqual(cart.item_count, 1)
def test_add_multiple_items_sums_prices(self):
cart = ShoppingCart()
cart.add_item("Apple", 1.00)
cart.add_item("Banana", 0.50)
self.assertAlmostEqual(cart.total, 1.50)
def test_empty_cart_has_zero_total(self):
cart = ShoppingCart()
self.assertEqual(cart.total, 0)
class ShoppingCart:
def __init__(self):
self.items: list[tuple[str, float]] = []
@property
def item_count(self) -> int:
return len(self.items)
@property
() -> :
(price _, price .items)
() -> :
.items.append((name, price))
:
():
._items: [CartItem] = []
() -> :
(._items)
() -> :
(item.price item ._items)
() -> :
._items.append(CartItem(name, price))
:
():
.name = name
.price = price
Mocking Dependencies
import unittest
from unittest.mock import Mock, patch
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
email: str
class UserService:
def __init__(self, user_repository, email_service):
self.user_repository = user_repository
self.email_service = email_service
def create_user(self, name: str, email: str) -> User:
user = User(
id=self.user_repository.get_next_id(),
name=name,
email=email
)
self.user_repository.save(user)
self.email_service.send_welcome_email(user)
return user
class TestUserService(unittest.TestCase):
def setUp(self):
self.mock_repo = Mock()
self.mock_email = Mock()
self.service = UserService(self.mock_repo, self.mock_email)
def test_creates_user_and_sends_email(self):
self.mock_repo.get_next_id.return_value =
user = .service.create_user(, )
.assertEqual(user., )
.assertEqual(user.name, )
.assertEqual(user.email, )
.mock_repo.save.assert_called_once()
.mock_email.send_welcome_email.assert_called_once_with(user)
():
.mock_repo.get_next_id.return_value =
.service.create_user(, )
saved_user = .mock_repo.save.call_args[][]
.assertEqual(saved_user.name, )
Testing Edge Cases
import unittest
class Stack:
def __init__(self):
self._items: list = []
def push(self, item: object) -> None:
self._items.append(item)
def pop(self) -> object:
if not self._items:
raise IndexError("pop from empty stack")
return self._items.pop()
def peek(self) -> object:
if not self._items:
raise IndexError("peek from empty stack")
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
def __len__(self) -> int:
return len(._items)
(unittest.TestCase):
():
.stack = Stack()
():
.stack.push()
.stack.push()
.assertEqual(.stack.pop(), )
.assertEqual(.stack.pop(), )
():
.stack.push()
.assertEqual(.stack.peek(), )
.assertEqual((.stack), )
():
.assertTrue(.stack.is_empty())
():
.assertRaises(IndexError):
.stack.pop()
():
.assertRaises(IndexError):
.stack.peek()
():
.stack.push()
.stack.push()
.stack.push()
.assertEqual((.stack), )
.assertEqual(.stack.pop(), )
.assertFalse(.stack.is_empty())
.assertEqual(.stack.pop(), )
.assertEqual(.stack.pop(), )
.assertTrue(.stack.is_empty())
Parameterized Tests
import unittest
from parameterized import parameterized
class Calculator:
def add(self, a: float, b: float) -> float:
return a + b
def subtract(self, a: float, b: float) -> float:
return a - b
def multiply(self, a: float, b: float) -> float:
return a * b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("Division by zero")
return a / b
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
@parameterized.expand([
(2, 3, 5),
(0, 0, 0),
(),
(),
(),
])
():
.assertEqual(.calc.add(a, b), expected)
():
.assertEqual(.calc.subtract(a, b), expected)
():
.assertEqual(.calc.multiply(a, b), expected)
():
.assertEqual(.calc.divide(a, b), expected)
():
.assertRaises(ValueError):
.calc.divide(, )
Testing with Fixtures
import unittest
from dataclasses import dataclass
from datetime import datetime, timedelta
@dataclass
class Order:
order_id: str
customer_id: str
items: list[dict]
created_at: datetime
status: str = "pending"
class OrderProcessor:
def __init__(self, discount_rules: dict, tax_rate: float):
self.discount_rules = discount_rules
self.tax_rate = tax_rate
def calculate_total(self, order: Order) -> float:
subtotal = sum(item["price"] * item["quantity"] for item in order["items"])
discount = self._calculate_discount(subtotal, order["customer_id"])
tax = (subtotal - discount) * self.tax_rate
return subtotal - discount + tax
def _calculate_discount(self, subtotal: float, customer_id: str) -> float:
if customer_id in self.discount_rules.get("vip", []):
subtotal *
(unittest.TestCase):
FIXTURE_VIP_CUSTOMERS = [, ]
FIXTURE_DISCOUNT_RULES = {: .FIXTURE_VIP_CUSTOMERS}
FIXTURE_TAX_RATE =
():
.processor = OrderProcessor(
.FIXTURE_DISCOUNT_RULES,
.FIXTURE_TAX_RATE
)
.base_order = Order(
order_id=,
customer_id=,
items=[
{: , : , : },
{: , : , : },
],
created_at=datetime.now()
)
():
order = .base_order
total = .processor.calculate_total(order)
.assertAlmostEqual(total, * )
():
vip_order = Order(
order_id=,
customer_id=,
items=[{: , : , : }],
created_at=datetime.now()
)
total = .processor.calculate_total(vip_order)
.assertAlmostEqual(total, * * )
Best Practices
- Write Failing Tests First: Start with a test that describes desired behavior
- Write Minimal Code: Pass the test with the simplest implementation
- Refactor After Each Test: Improve design while tests protect you
- Test One Thing: Each test should verify a single behavior
- Use Descriptive Names: Test method names should describe what's tested
- Fast Tests: Unit tests should run in milliseconds
- Isolate Tests: Each test should be independent
- Avoid Logic in Tests: No conditionals or loops in test bodies
- Test Edge Cases: Empty, null, boundary conditions
- Use Test Doubles: Mocks, stubs, fakes for isolation
- Follow FIRST: Fast, Isolated, Repeatable, Self-validating, Timely
- Aim for High Coverage: But remember 100% coverage doesn't mean good tests