14 KiB
Testing Patterns for TDD
This document provides language-agnostic testing patterns and best practices that support effective Test-Driven Development across different programming environments.
Test Structure Patterns
The AAA Pattern (Arrange-Act-Assert)
The most fundamental testing pattern. Every test should follow this three-part structure:
Arrange: Set up the test data and preconditions
Act: Execute the behavior being tested
Assert: Verify the expected outcome
Example (Python):
def test_should_add_item_to_shopping_cart():
# Arrange
cart = ShoppingCart()
item = Item("Book", price=29.99)
# Act
cart.add_item(item)
# Assert
assert len(cart.items) == 1
assert cart.items[0] == item
Example (JavaScript):
test('should add item to shopping cart', () => {
// Arrange
const cart = new ShoppingCart();
const item = new Item("Book", 29.99);
// Act
cart.addItem(item);
// Assert
expect(cart.items).toHaveLength(1);
expect(cart.items[0]).toBe(item);
});
Given-When-Then (BDD Style)
An alternative to AAA, commonly used in Behavior-Driven Development:
Given: Preconditions and context
When: The action or event
Then: Expected outcomes
Example (Ruby):
describe 'Shopping Cart' do
it 'adds item to cart' do
# Given a cart and an item
cart = ShoppingCart.new
item = Item.new("Book", 29.99)
# When adding the item
cart.add_item(item)
# Then the cart should contain the item
expect(cart.items.length).to eq(1)
expect(cart.items[0]).to eq(item)
end
end
Test Organization Patterns
Test Fixture Pattern
Use setup/teardown to create consistent test preconditions:
Example (Python with pytest):
import pytest
@pytest.fixture
def cart():
"""Create a fresh shopping cart for each test"""
return ShoppingCart()
@pytest.fixture
def sample_items():
"""Create sample items for testing"""
return [
Item("Book", 29.99),
Item("Pen", 1.99),
Item("Notebook", 5.99)
]
def test_should_calculate_correct_total(cart, sample_items):
for item in sample_items:
cart.add_item(item)
total = cart.calculate_total()
assert total == 37.97
Example (JavaScript with Jest):
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart();
});
test('should calculate correct total', () => {
cart.addItem(new Item("Book", 29.99));
cart.addItem(new Item("Pen", 1.99));
const total = cart.calculateTotal();
expect(total).toBe(31.98);
});
});
Test Builder Pattern
Create fluent APIs for complex test data setup:
Example (Java):
public class OrderBuilder {
private Customer customer;
private List<Item> items = new ArrayList<>();
private PaymentMethod paymentMethod;
public OrderBuilder withCustomer(String name, String email) {
this.customer = new Customer(name, email);
return this;
}
public OrderBuilder withItem(String name, double price) {
this.items.add(new Item(name, price));
return this;
}
public OrderBuilder withPayment(PaymentMethod method) {
this.paymentMethod = method;
return this;
}
public Order build() {
Order order = new Order(customer);
items.forEach(order::addItem);
order.setPaymentMethod(paymentMethod);
return order;
}
}
// Usage in tests:
@Test
public void shouldProcessOrderSuccessfully() {
Order order = new OrderBuilder()
.withCustomer("John Doe", "john@example.com")
.withItem("Book", 29.99)
.withItem("Pen", 1.99)
.withPayment(PaymentMethod.CREDIT_CARD)
.build();
OrderResult result = processor.process(order);
assertEquals(OrderStatus.COMPLETED, result.getStatus());
}
Object Mother Pattern
Centralized factory for creating common test objects:
Example (Python):
class CustomerMother:
"""Factory for creating test customers"""
@staticmethod
def create_standard_customer():
return Customer(
name="John Doe",
email="john@example.com",
is_active=True
)
@staticmethod
def create_vip_customer():
return Customer(
name="Jane Smith",
email="jane@example.com",
is_active=True,
membership_level="VIP"
)
@staticmethod
def create_inactive_customer():
return Customer(
name="Bob Wilson",
email="bob@example.com",
is_active=False
)
# Usage in tests:
def test_should_apply_vip_discount():
customer = CustomerMother.create_vip_customer()
cart = ShoppingCart(customer)
cart.add_item(Item("Book", 100))
total = cart.calculate_total()
assert total == 80 # 20% VIP discount
Assertion Patterns
Single Assertion Principle
Guideline: Each test should verify one logical concept (though may have multiple assertion statements for clarity).
Good:
def test_should_create_user_with_correct_attributes():
user = create_user("john@example.com", "John Doe")
# Multiple assertions verifying one concept: user creation
assert user.email == "john@example.com"
assert user.name == "John Doe"
assert user.is_active is True
Better (when concepts are truly separate):
def test_should_create_user_with_provided_email():
user = create_user("john@example.com", "John Doe")
assert user.email == "john@example.com"
def test_should_create_user_with_provided_name():
user = create_user("john@example.com", "John Doe")
assert user.name == "John Doe"
def test_should_create_active_user_by_default():
user = create_user("john@example.com", "John Doe")
assert user.is_active is True
Custom Assertion Methods
Create domain-specific assertions for clarity:
Example (JavaScript):
function assertValidOrder(order) {
expect(order.customer).toBeDefined();
expect(order.items).not.toHaveLength(0);
expect(order.total).toBeGreaterThan(0);
expect(order.status).toBe('pending');
}
test('should create valid order from cart', () => {
const cart = createSampleCart();
const order = cart.checkout();
assertValidOrder(order);
});
Test Doubles (Mocking) Patterns
Stub Pattern
Replace dependencies with simplified implementations:
Example (Python):
class StubEmailService:
"""Stub that tracks calls without sending real emails"""
def __init__(self):
self.sent_emails = []
def send(self, to, subject, body):
self.sent_emails.append({
'to': to,
'subject': subject,
'body': body
})
def test_should_send_welcome_email_on_registration():
email_service = StubEmailService()
user_service = UserService(email_service)
user = user_service.register("john@example.com", "password")
assert len(email_service.sent_emails) == 1
assert email_service.sent_emails[0]['to'] == "john@example.com"
assert "Welcome" in email_service.sent_emails[0]['subject']
Mock Pattern
Verify interactions with dependencies:
Example (JavaScript with Jest):
test('should call payment gateway with correct amount', () => {
const mockGateway = {
charge: jest.fn().mockResolvedValue({ success: true })
};
const processor = new PaymentProcessor(mockGateway);
processor.processPayment(customer, 100.00);
expect(mockGateway.charge).toHaveBeenCalledWith(
customer.paymentToken,
100.00
);
});
Fake Pattern
Provide working implementations with shortcuts:
Example (Python):
class FakeUserRepository:
"""In-memory repository for testing"""
def __init__(self):
self.users = {}
self.next_id = 1
def save(self, user):
if not user.id:
user.id = self.next_id
self.next_id += 1
self.users[user.id] = user
return user
def find_by_id(self, user_id):
return self.users.get(user_id)
def test_should_persist_user_with_generated_id():
repo = FakeUserRepository()
user = User(name="John")
saved_user = repo.save(user)
assert saved_user.id is not None
assert repo.find_by_id(saved_user.id) == saved_user
Parameterized Testing Pattern
Test multiple cases with the same structure:
Example (Python with pytest):
import pytest
@pytest.mark.parametrize("input,expected", [
(0, 0),
(1, 1),
(2, 2),
(3, 6),
(4, 24),
(5, 120),
])
def test_factorial_calculation(input, expected):
result = factorial(input)
assert result == expected
Example (JavaScript with Jest):
test.each([
[0, 0],
[1, 1],
[2, 2],
[3, 6],
[4, 24],
[5, 120],
])('factorial(%i) should equal %i', (input, expected) => {
const result = factorial(input);
expect(result).toBe(expected);
});
Exception Testing Pattern
Test error conditions explicitly:
Example (Python):
def test_should_raise_error_when_withdrawing_too_much():
account = Account(balance=100)
with pytest.raises(InsufficientFundsError) as exc_info:
account.withdraw(150)
assert "Insufficient funds" in str(exc_info.value)
assert exc_info.value.available == 100
assert exc_info.value.requested == 150
Example (JavaScript):
test('should throw error when withdrawing too much', () => {
const account = new Account(100);
expect(() => {
account.withdraw(150);
}).toThrow(InsufficientFundsError);
expect(() => {
account.withdraw(150);
}).toThrow('Insufficient funds');
});
Property-Based Testing Pattern
Test properties that should always hold:
Example (Python with hypothesis):
from hypothesis import given
import hypothesis.strategies as st
@given(st.lists(st.integers()))
def test_reversing_twice_gives_original(lst):
result = reverse(reverse(lst))
assert result == lst
@given(st.integers(min_value=0))
def test_factorial_is_positive(n):
result = factorial(n)
assert result > 0
State-Based vs. Interaction-Based Testing
State-Based Testing (Preferred)
Verify final state rather than how it was achieved:
Example:
def test_should_remove_item_from_cart():
cart = ShoppingCart()
item = Item("Book", 29.99)
cart.add_item(item)
cart.remove_item(item)
assert len(cart.items) == 0 # Verify state
Interaction-Based Testing
Verify interactions when state is not observable:
Example:
def test_should_log_failed_login_attempt():
logger = Mock()
auth = AuthService(logger)
auth.login("user", "wrong_password")
logger.warning.assert_called_once_with(
"Failed login attempt for user: user"
)
Test Naming Patterns
Should-Style Naming
test_should_<expected_behavior>_when_<condition>
Examples:
test_should_return_empty_list_when_no_matches_foundtest_should_throw_exception_when_amount_is_negativetest_should_apply_discount_when_quantity_exceeds_ten
Behavior-Style Naming
test_<subject>_<scenario>_<expected_result>
Examples:
test_cart_with_multiple_items_calculates_correct_totaltest_user_registration_with_invalid_email_failstest_payment_processing_with_insufficient_funds_raises_error
Test Data Patterns
Obvious Data
Use self-documenting test data:
Bad:
def test_calculation():
result = calculate(10, 5, 3)
assert result == 8
Good:
def test_should_calculate_average_correctly():
value1 = 10
value2 = 20
value3 = 30
expected_average = 20
result = calculate_average([value1, value2, value3])
assert result == expected_average
Boundary Value Testing
Test edges of valid ranges:
def test_age_validation():
validator = AgeValidator(min_age=18, max_age=100)
# Below minimum
assert not validator.is_valid(17)
# At minimum boundary
assert validator.is_valid(18)
# Normal value
assert validator.is_valid(50)
# At maximum boundary
assert validator.is_valid(100)
# Above maximum
assert not validator.is_valid(101)
TDD-Specific Patterns
Triangulation
Build generality through multiple test cases:
Step 1 - Specific case:
def test_should_add_two_numbers():
assert add(2, 3) == 5
# Implementation:
def add(a, b):
return 5 # Hardcoded - simplest thing that works
Step 2 - Add another case to force generalization:
def test_should_add_two_numbers():
assert add(2, 3) == 5
def test_should_add_different_numbers():
assert add(4, 7) == 11
# Implementation:
def add(a, b):
return a + b # Now forced to generalize
Fake It Till You Make It
Start with simple/hardcoded implementations:
# Test
def test_should_return_greeting():
assert greet("World") == "Hello, World!"
# First implementation (fake it)
def greet(name):
return "Hello, World!"
# Add another test to force real implementation
def test_should_return_greeting_with_different_name():
assert greet("Alice") == "Hello, Alice!"
# Real implementation
def greet(name):
return f"Hello, {name}!"
Obvious Implementation
When the implementation is obvious, just write it:
# Test
def test_should_calculate_rectangle_area():
assert calculate_area(width=5, height=3) == 15
# Implementation (obvious, no need to fake)
def calculate_area(width, height):
return width * height
Summary: Testing Pattern Selection
- Use AAA for clarity in most tests
- Use fixtures to eliminate setup duplication
- Use builders for complex object creation
- Use test doubles when dependencies are expensive or unpredictable
- Prefer state-based testing over interaction-based
- Use parameterized tests for multiple similar cases
- Test boundaries explicitly
- Name tests to describe behavior
- Keep tests independent of each other
- Make tests readable - they're documentation
Remember: Tests are first-class citizens in TDD. Treat them with the same care as production code.