Test-Driven Development methodology, red-green-refactor cycle, unit testing best practices, and mocking strategies
Scope: Comprehensive guide to TDD methodology, red-green-refactor cycle, unit testing, mocking, and test design Lines: ~380 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
The TDD Mantra: Red → Green → Refactor
1. RED: Write a failing test
↓
2. GREEN: Write minimal code to make it pass
↓
3. REFACTOR: Improve code without changing behavior
↓
4. REPEAT
Rules:
Example Cycle:
# RED: Write failing test
def test_add_two_numbers():
calculator = Calculator()
assert calculator.add(2, 3) == 5 # Fails - Calculator doesn't exist
# GREEN: Minimal code to pass
class Calculator:
def add(self, a, b):
return 5 # Hardcoded - but test passes!
# RED: Write another test
def test_add_different_numbers():
calculator = Calculator()
assert calculator.add(10, 20) == 30 # Fails with hardcoded 5
# GREEN: Real implementation
class Calculator:
def add(self, a, b):
return a + b # Now all tests pass
# REFACTOR: Clean up if needed (none needed here)
Arrange-Act-Assert:
def test_user_login():
# Arrange: Set up test data
user = User(email="test@example.com", password="secret123")
user.save()
# Act: Execute the action being tested
result = authenticate(email="test@example.com", password="secret123")
# Assert: Verify the result
assert result.success is True
assert result.user.email == "test@example.com"
Given-When-Then (BDD style):
describe("User Login", () => {
it("should authenticate user with correct credentials", () => {
// Given: A registered user
const user = createUser({ email: "test@example.com", password: "secret" });
// When: User attempts to login
const result = authenticate("test@example.com", "secret");
// Then: Authentication succeeds
expect(result.success).toBe(true);
expect(result.user.email).toBe("test@example.com");
});
});
Stub: Returns predefined data
class StubEmailService:
def send(self, to, subject, body):
return True # Always succeeds
Mock: Verifies interactions
mock_email = Mock()
service.send_welcome_email(user)
mock_email.send.assert_called_once_with(
to="user@example.com",
subject="Welcome!",
body="Welcome to our app!"
)
Fake: Working implementation (simplified)
class FakeDatabase:
def __init__(self):
self.data = {}
def save(self, key, value):
self.data[key] = value
def get(self, key):
return self.data.get(key)
Pure Function: Same inputs → Same outputs, no side effects
# RED: Write test first
def test_calculate_tax():
assert calculate_tax(100, 0.08) == 8.0
assert calculate_tax(50, 0.10) == 5.0
assert calculate_tax(0, 0.08) == 0.0
# GREEN: Implement
def calculate_tax(amount: float, rate: float) -> float:
return amount * rate
# REFACTOR: Add edge case tests
def test_calculate_tax_negative():
with pytest.raises(ValueError):
calculate_tax(-100, 0.08)
def calculate_tax(amount: float, rate: float) -> float:
if amount < 0:
raise ValueError("Amount cannot be negative")
return amount * rate
Before TDD (hard to test):
class UserService:
def create_user(self, email):
db = PostgresDatabase() # Hard-coded dependency!
email_service = SendGridEmailService() # Another one!
user = db.save({"email": email})
email_service.send(user.email, "Welcome!")
return user
TDD Approach (test first drives design):
# RED: Write test with mocks
def test_create_user():
mock_db = Mock()
mock_email = Mock()
mock_db.save.return_value = User(id=1, email="test@example.com")
service = UserService(db=mock_db, email_service=mock_email)
user = service.create_user("test@example.com")
# Verify interactions
mock_db.save.assert_called_once_with({"email": "test@example.com"})
mock_email.send.assert_called_once_with("test@example.com", "Welcome!")
assert user.email == "test@example.com"
# GREEN: Implement with dependency injection
class UserService:
def __init__(self, db, email_service):
self.db = db
self.email_service = email_service
def create_user(self, email):
user = self.db.save({"email": email})
self.email_service.send(user.email, "Welcome!")
return user
TDD Forced Good Design: Dependency injection makes code testable!
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
amount float64
userType string
want float64
}{
{"Premium user, large purchase", 1000, "premium", 200},
{"Premium user, small purchase", 50, "premium", 5},
{"Regular user, large purchase", 1000, "regular", 100},
{"Regular user, small purchase", 50, "regular", 0},
{"Guest user", 1000, "guest", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateDiscount(tt.amount, tt.userType)
if got != tt.want {
t.Errorf("CalculateDiscount(%v, %v) = %v, want %v",
tt.amount, tt.userType, got, tt.want)
}
})
}
}
Python (async/await):
import pytest
@pytest.mark.asyncio
async def test_fetch_user_data():
# Arrange
user_id = "123"
mock_api = AsyncMock()
mock_api.get_user.return_value = {"id": "123", "name": "John"}
service = UserService(api=mock_api)
# Act
user = await service.fetch_user_data(user_id)
# Assert
assert user["name"] == "John"
mock_api.get_user.assert_called_once_with(user_id)
TypeScript (Promises):
describe("fetchUserData", () => {
it("should fetch user from API", async () => {
// Arrange
const mockApi = {
getUser: jest.fn().mockResolvedValue({ id: "123", name: "John" }),
};
const service = new UserService(mockApi);
// Act
const user = await service.fetchUserData("123");
// Assert
expect(user.name).toBe("John");
expect(mockApi.getUser).toHaveBeenCalledWith("123");
});
});
def test_divide_by_zero():
calculator = Calculator()
with pytest.raises(ZeroDivisionError):
calculator.divide(10, 0)
def test_invalid_email():
with pytest.raises(ValidationError) as exc_info:
create_user(email="invalid-email")
assert "Invalid email format" in str(exc_info.value)
def test_user_not_found():
result = find_user(user_id="nonexistent")
assert result is None # Or raises UserNotFoundError
Python (pytest):
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("WORLD", "WORLD"),
("TeSt", "TEST"),
("", ""),
])
def test_uppercase(input, expected):
assert uppercase(input) == expected
TypeScript (Jest):
test.each([
[1, 1, 2],
[2, 2, 4],
[5, 3, 8],
])("add(%i, %i) should return %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
Python (pytest fixtures):
@pytest.fixture
def sample_user():
"""Create a test user for multiple tests"""
return User(email="test@example.com", name="Test User")
@pytest.fixture
def database():
"""Set up and tear down test database"""
db = create_test_database()
yield db
db.drop_all()
def test_save_user(database, sample_user):
database.save(sample_user)
assert database.count() == 1
def test_find_user(database, sample_user):
database.save(sample_user)
found = database.find_by_email("test@example.com")
assert found.name == "Test User"
TypeScript (Jest):
describe("UserService", () => {
let service: UserService;
let mockDb: Database;
beforeEach(() => {
mockDb = createMockDatabase();
service = new UserService(mockDb);
});
afterEach(() => {
mockDb.clear();
});
it("should save user", () => {
const user = { email: "test@example.com" };
service.save(user);
expect(mockDb.count()).toBe(1);
});
});
Bad Approach:
# DON'T: Testing private methods directly
def test_private_validation():
obj = MyClass()
assert obj._validate_input("test") is True # Testing private method
Good Approach:
# DO: Test through public interface
def test_public_method_with_valid_input():
obj = MyClass()
result = obj.process("test") # Public method
assert result.success is True # Private validation tested indirectly
def test_public_method_with_invalid_input():
obj = MyClass()
result = obj.process("") # Public method
assert result.success is False # Private validation tested indirectly
Principle: Tests should verify behavior, not implementation details
Requirement: Create a calculator that adds numbers in a string
# Iteration 1: Empty string
def test_empty_string_returns_zero():
assert string_calculator("") == 0
def string_calculator(numbers):
return 0 # Minimal code to pass
# Iteration 2: Single number
def test_single_number():
assert string_calculator("5") == 5
def string_calculator(numbers):
if not numbers:
return 0
return int(numbers)
# Iteration 3: Two numbers
def test_two_numbers():
assert string_calculator("1,2") == 3
def string_calculator(numbers):
if not numbers:
return 0
return sum(int(n) for n in numbers.split(","))
# Iteration 4: Multiple numbers
def test_multiple_numbers():
assert string_calculator("1,2,3,4") == 10
# Already passes with current implementation!
# Iteration 5: New line delimiter
def test_newline_delimiter():
assert string_calculator("1\n2,3") == 6
def string_calculator(numbers):
if not numbers:
return 0
numbers = numbers.replace("\n", ",")
return sum(int(n) for n in numbers.split(","))
# Iteration 6: Negative numbers throw exception
def test_negative_numbers_throw():
with pytest.raises(ValueError, match="Negatives not allowed: -2, -5"):
string_calculator("1,-2,3,-5")
def string_calculator(numbers):
if not numbers:
return 0
numbers = numbers.replace("\n", ",")
nums = [int(n) for n in numbers.split(",")]
negatives = [n for n in nums if n < 0]
if negatives:
raise ValueError(f"Negatives not allowed: {', '.join(map(str, negatives))}")
return sum(nums)
TDD Benefits Demonstrated:
# RED: Test first
def test_create_user_endpoint(client):
response = client.post("/users", json={
"email": "test@example.com",
"name": "Test User"
})
assert response.status_code == 201
assert response.json["email"] == "test@example.com"
assert "id" in response.json
# GREEN: Implement minimal endpoint
@app.post("/users")
def create_user(request):
data = request.json
user = User(email=data["email"], name=data["name"])
db.save(user)
return {"id": user.id, "email": user.email}, 201
# RED: Test validation
def test_create_user_invalid_email(client):
response = client.post("/users", json={
"email": "invalid-email",
"name": "Test"
})
assert response.status_code == 400
assert "Invalid email" in response.json["error"]
# GREEN: Add validation
@app.post("/users")
def create_user(request):
data = request.json
if not is_valid_email(data["email"]):
return {"error": "Invalid email format"}, 400
user = User(email=data["email"], name=data["name"])
db.save(user)
return {"id": user.id, "email": user.email}, 201
Do's:
Don'ts:
Good Test Names:
def test_add_returns_sum_of_two_numbers()
def test_divide_raises_error_when_divisor_is_zero()
def test_create_user_saves_to_database()
def test_authenticate_fails_with_invalid_password()
Pattern: test_[method]_[scenario]_[expected_result]
❌ Writing all tests upfront
→ Write one test at a time, see it fail, make it pass
❌ Testing getters/setters
→ Test actual behavior, not trivial code
❌ Overly complex test setup
→ Simplify with fixtures or factory functions
❌ Tests that depend on each other
→ Each test should be independent
❌ Not seeing tests fail first
→ Always see red before green (ensures test works)
❌ Skipping refactor step
→ Refactor to keep code clean
❌ Testing implementation details
→ Test through public interface
❌ Slow tests
→ Use mocks/stubs for fast unit tests
# pytest: Most popular Python test framework
pytest tests/
# Coverage
pytest --cov=src --cov-report=html
# Watch mode (rerun on changes)
pytest-watch
# Mocking
from unittest.mock import Mock, patch
# Jest: Popular testing framework
npm test
# Watch mode
npm test -- --watch
# Coverage
npm test -- --coverage
# Mocking
jest.mock('./module')
# Built-in testing
go test ./...
# Coverage
go test -cover ./...
# Table-driven tests (idiomatic Go)
# Continuous testing
npm install -g wallaby.js # IDE integration
npm install -g jest --watch # CLI watch mode
Location: skills/engineering/test-driven-development/resources/REFERENCE.md (~900 lines)
Complete reference covering:
Location: skills/engineering/test-driven-development/resources/scripts/
1. TDD Session Tracker (tdd_session.py)
# Start interactive TDD session
./tdd_session.py start
# Analyze completed session
./tdd_session.py analyze --session-file session.json
# Get session statistics
./tdd_session.py stats --session-file session.json --json
2. TDD Metrics Analyzer (analyze_tdd_metrics.py)
# Calculate test-to-code ratio
./analyze_tdd_metrics.py ratio --source-dir src/ --test-dir tests/
# Analyze TDD health
./analyze_tdd_metrics.py health --source-dir src/ --test-dir tests/ --coverage-file .coverage
# Track metrics across commits
./analyze_tdd_metrics.py history --repo-dir . --commits 10 --json
3. Test Template Generator (generate_test_template.sh)
# Python pytest template
./generate_test_template.sh python Calculator --framework pytest
# TypeScript Jest template with BDD style
./generate_test_template.sh typescript UserService --style bdd
# Rust test template
./generate_test_template.sh rust string_utils
# Go table-driven test template
./generate_test_template.sh go Validator
Location: skills/engineering/test-driven-development/resources/examples/
Python (python/tdd_example.py)
TypeScript (typescript/tdd_example.test.ts)
Rust (rust/tdd_example.rs)
Workflow Guide (workflows/tdd-workflow.md)
Start a TDD session:
cd skills/engineering/test-driven-development/resources
./scripts/tdd_session.py start
Generate test template for new feature:
./scripts/generate_test_template.sh python UserAuthentication --framework pytest --style bdd
Check TDD health:
./scripts/analyze_tdd_metrics.py health \
--source-dir ../../../src \
--test-dir ../../../tests \
--coverage-file ../../../.coverage
Study examples:
# Python example with full TDD progression
cat examples/python/tdd_example.py
# TypeScript kata
cat examples/typescript/tdd_example.test.ts
# Rust with type system
cat examples/rust/tdd_example.rs
# Complete workflow guide
cat examples/workflows/tdd-workflow.md