## The Red-Green-Refactor Cycle
```
TDD follows a strict cycle:
1. 🔴 RED: Write a failing test
- Test doesn't exist yet
- Run test → it should FAIL
2. 🟢 GREEN: Write minimal code to pass
- Implement just enough to make test pass
- Don't over-engineer
- Run test → it should PASS
3. 🔄 REFACTOR: Improve the code
- Clean up implementation
- Remove duplication
- Improve readability
- Run tests → they should still PASS
```
## TDD Workflow in Practice
### Step 1: Understand the Requirement
```
Before writing any code:
1. Clarify what needs to be built
2. Break into small, testable behaviors
3. Identify edge cases
4. Determine expected inputs/outputs
Example requirement: "Create a function that validates email addresses"
Behaviors to test:
- Valid email returns true
- Invalid email returns false
- Empty string returns false
- Email without @ returns false
- Email without domain returns false
```
### Step 2: Write the First Failing Test
```python
import pytest
from email_validator import is_valid_email
def test_valid_email_returns_true():
"""A properly formatted email should be valid."""
assert is_valid_email("user@example.com") == True
```
```javascript
// email-validator.test.js
const { isValidEmail } = require('./email-validator');
describe('isValidEmail', () => {
test('valid email returns true', () => {
expect(isValidEmail('user@example.com')).toBe(true);
});
});
```
**Run the test → It should FAIL (function doesn't exist)**
### Step 3: Write Minimal Implementation
```python
def is_valid_email(email: str) -> bool:
return "@" in email and "." in email
```
```javascript
// email-validator.js
function isValidEmail(email) {
return email.includes('@') && email.includes('.');
}
module.exports = { isValidEmail };
```
**Run the test → It should PASS**
### Step 4: Add More Tests
```python
def test_email_without_at_returns_false():
"""Email missing @ symbol is invalid."""
assert is_valid_email("userexample.com") == False
def test_empty_string_returns_false():
"""Empty string is invalid."""
assert is_valid_email("") == False
def test_email_without_domain_returns_false():
"""Email without domain is invalid."""
assert is_valid_email("user@") == False
def test_email_with_multiple_at_returns_false():
"""Email with multiple @ is invalid."""
assert is_valid_email("user@@example.com") == False
```
### Step 5: Refactor to Pass All Tests
```python
import re
def is_valid_email(email: str) -> bool:
"""
Validate email address format.
Args:
email: The email address to validate
Returns:
True if email is valid, False otherwise
"""
if not email or not isinstance(email, str):
return False
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
```
## TDD Patterns
### Arrange-Act-Assert (AAA)
```python
def test_user_creation():
user_data = {"name": "Alice", "email": "alice@example.com"}
user = User.create(user_data)
assert user.name == "Alice"
assert user.email == "alice@example.com"
assert user.id is not None
```
### Given-When-Then (BDD Style)
```python
def test_user_can_update_profile():
user = create_test_user(name="Alice")
user.update_profile(name="Alicia")
assert user.name == "Alicia"
```
### Test Isolation with Mocking
```python
from unittest.mock import Mock, patch
def test_sends_email_on_registration():
"""Verify email is sent when user registers."""
with patch('services.email.send') as mock_send:
register_user("alice@example.com")
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome!",
body=mock.ANY
)
```
### Parameterized Tests
```python
import pytest
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("user.name@example.co.uk", True),
("invalid", False),
("@example.com", False),
("user@", False),
("", False),
(None, False),
])
def test_email_validation(email, expected):
assert is_valid_email(email) == expected
```
## Testing Strategies by Code Type
### Pure Functions
```python
def test_calculate_total():
items = [{"price": 10}, {"price": 20}]
assert calculate_total(items) == 30
```
### Classes with State
```python
def test_shopping_cart_lifecycle():
cart = ShoppingCart()
assert cart.total == 0
cart.add_item(Item(price=10))
cart.add_item(Item(price=20))
assert cart.total == 30
cart.remove_item(0)
assert cart.total == 20
```
### External Dependencies
```python
def test_fetches_user_from_api():
"""Test API integration with mock."""
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {
"id": 1,
"name": "Alice"
}
mock_get.return_value.status_code = 200
user = fetch_user(1)
assert user["name"] == "Alice"
mock_get.assert_called_with("/api/users/1")
```
### Async Code
```python
import pytest
@pytest.mark.asyncio
async def test_async_data_fetch():
result = await fetch_data_async()
assert result is not None
assert "data" in result
```
## Common TDD Mistakes to Avoid
```
❌ Writing too many tests at once
→ Write ONE test, make it pass, repeat
❌ Writing implementation before tests
→ Always test first, even if you know the answer
❌ Making tests pass with hardcoded values
→ Use real logic, refactor after
❌ Testing implementation details
→ Test behavior, not internal structure
❌ Skipping the refactor step
→ Always clean up after going green
❌ Not running tests frequently
→ Run after every small change
```
## TDD Command Cheatsheet
```bash
pytest # Run all tests
pytest -v # Verbose output
pytest -x # Stop on first failure
pytest --tb=short # Short tracebacks
pytest -k "test_email" # Run matching tests
pytest --cov=src # With coverage
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # With coverage
npm test -- -t "email" # Run matching tests
go test ./... # Run all tests
go test -v # Verbose
go test -cover # With coverage
go test -run TestEmail # Run matching tests
```
## Best Practices
1. **One Assertion Per Test** (when practical): Tests should verify one specific behavior
2. **Descriptive Test Names**: `test_user_with_invalid_email_is_rejected`
3. **Independent Tests**: Tests shouldn't depend on order or shared state
4. **Fast Tests**: Unit tests should run in milliseconds
5. **Meaningful Failures**: Error messages should indicate what went wrong
6. **Test Edge Cases**: Empty inputs, nulls, boundaries, error conditions
## Related Resources
- [Test-Driven Development by Kent Beck](https://www.oreilly.com/library/view/test-driven-development/0321146530/)
- [pytest Documentation](https://docs.pytest.org/)
- [Jest Documentation](https://jestjs.io/)
- [The Art of Unit Testing](https://www.manning.com/books/the-art-of-unit-testing-third-edition)