TDD with Python
Introduction
Test-Driven Development (TDD) is one of the most powerful practices a developer can adopt. After 10+ years in software development, I’ve seen firsthand how TDD transforms not just code quality, but the entire development process.
In this post, I’ll share practical insights on implementing TDD in Python projects, based on my experience building products from 0→1 and scaling them to enterprise level.
What is TDD?
TDD follows a simple three-step cycle:
- Red - Write a failing test
- Green - Write minimal code to pass the test
- Refactor - Improve the code while keeping tests green
Why TDD Matters in Startups
Having worked extensively with startups, I’ve learned that TDD is crucial when:
- Building MVPs that need to evolve quickly
- Scaling teams and onboarding new developers
- Refactoring legacy systems
- Ensuring reliability under rapid growth
A Practical Python Example
Let’s build a simple user authentication system using TDD:
Step 1: Write the Test First
# test_auth.py
import pytest
from auth import UserAuthenticator, InvalidCredentialsError
class TestUserAuthenticator:
def setup_method(self):
self.auth = UserAuthenticator()
def test_successful_login(self):
# Arrange
username = "testuser"
password = "securepassword"
self.auth.register_user(username, password)
# Act
result = self.auth.login(username, password)
# Assert
assert result.is_authenticated is True
assert result.username == username
def test_login_with_invalid_password(self):
# Arrange
username = "testuser"
password = "securepassword"
wrong_password = "wrongpassword"
self.auth.register_user(username, password)
# Act & Assert
with pytest.raises(InvalidCredentialsError):
self.auth.login(username, wrong_password)Step 2: Write Minimal Implementation
# auth.py
import hashlib
from dataclasses import dataclass
from typing import Dict
class InvalidCredentialsError(Exception):
"""Raised when login credentials are invalid"""
pass
@dataclass
class LoginResult:
is_authenticated: bool
username: str
class UserAuthenticator:
def __init__(self):
self._users: Dict[str, str] = {}
def register_user(self, username: str, password: str) -> None:
"""Register a new user with hashed password"""
hashed_password = self._hash_password(password)
self._users[username] = hashed_password
def login(self, username: str, password: str) -> LoginResult:
"""Authenticate user and return login result"""
if username not in self._users:
raise InvalidCredentialsError("Invalid username or password")
hashed_password = self._hash_password(password)
if self._users[username] != hashed_password:
raise InvalidCredentialsError("Invalid username or password")
return LoginResult(is_authenticated=True, username=username)
def _hash_password(self, password: str) -> str:
"""Hash password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()Step 3: Run the Tests
pytest test_auth.py -vKey TDD Benefits I’ve Observed
1. Faster Development Cycles
- Immediate feedback on code changes
- Reduced debugging time
- Confident refactoring
2. Better Code Design
- Forces you to think about interfaces first
- Naturally leads to modular, testable code
- Prevents over-engineering
3. Living Documentation
- Tests serve as executable specifications
- New team members understand requirements quickly
- API contracts are clearly defined
Common TDD Challenges & Solutions
Challenge 1: “TDD is Too Slow”
Reality: TDD feels slower initially but pays dividends in:
- Reduced bug fixing time
- Faster feature additions
- Easier maintenance
Challenge 2: “What to Test?”
Focus on:
- Public API contracts
- Business logic
- Edge cases and error conditions
Avoid testing:
- Implementation details
- Third-party library functionality
- Simple getters/setters
Challenge 3: “Mocking is Complex”
# Use dependency injection for easier testing
class EmailService:
def send_email(self, to: str, subject: str, body: str) -> bool:
# Implementation here
pass
class UserRegistration:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def register(self, username: str, email: str) -> bool:
# Registration logic
return self.email_service.send_email(
email,
"Welcome!",
f"Welcome {username}!"
)
# In tests, inject a mock
def test_user_registration_sends_email():
mock_email_service = Mock()
mock_email_service.send_email.return_value = True
registration = UserRegistration(mock_email_service)
result = registration.register("testuser", "test@example.com")
assert result is True
mock_email_service.send_email.assert_called_once()Tools and Setup
Essential Python Testing Tools:
pip install pytest pytest-cov pytest-mockMy Recommended Test Structure:
project/
├── src/
│ └── myproject/
│ ├── __init__.py
│ └── auth.py
├── tests/
│ ├── __init__.py
│ ├── test_auth.py
│ └── conftest.py
├── pytest.ini
└── requirements-dev.txtpytest Configuration (pytest.ini):
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--strict-markers
--strict-config
--verbose
--cov=src
--cov-report=html
--cov-report=term-missingReal-World TDD Tips
1. Start Small
Begin with simple functions and gradually apply TDD to more complex systems.
2. Keep Tests Simple
Each test should verify one specific behavior.
3. Use Descriptive Test Names
def test_login_fails_when_user_does_not_exist(self):
def test_registration_sends_welcome_email_to_new_user(self):
def test_password_must_be_at_least_8_characters(self):4. Follow the AAA Pattern
- Arrange: Set up test data
- Act: Execute the behavior
- Assert: Verify the outcome
Conclusion
TDD isn’t just about testing—it’s a design methodology that leads to better software architecture. In my experience building products from concept to scale, TDD has been instrumental in:
- Maintaining code quality during rapid development
- Enabling confident refactoring of legacy systems
- Onboarding new team members effectively
- Building trust with stakeholders through reliable deliveries
The initial investment in learning TDD pays enormous dividends in code quality, development speed, and team confidence.
What’s your experience with TDD? I’d love to hear about your challenges and successes in the comments below!
Further Reading
Want to discuss TDD strategies for your startup? Reach out - I love talking shop about engineering practices!