Tutorial Overview
Series index: Superpowers Tutorial Series
test-driven-development is one of the core and strictest skills in the Superpowers framework. It forces AI to follow the classic TDD flow: write tests first, watch them fail, then write code.
What you will learn
- ✅ The RED-GREEN-REFACTOR loop
- ✅ How Superpowers enforces TDD
- ✅ Reasonable test coverage standards
- ✅ Common anti-patterns and how to avoid them
- ✅ A practical end-to-end example
Why Is TDD So Important?
Problems with traditional development
flowchart LR
A[Write code] --> B[Manual testing]
B --> C[Find a bug]
C --> D[Fix the bug]
D --> E[Introduce a new bug]
E --> F[Regression tests fail]
style A fill:#ffcccc
style E fill:#ffcccc
style F fill:#ffcccc
The TDD flow
flowchart TD
A[RED: Write a failing test] --> B[GREEN: Write the minimum code to pass]
B --> C[REFACTOR: Improve the code]
C --> D{Any more requirements?}
D -->|Yes | A
D -->|No | E[Done]
style A fill:#ffebee
style B fill:#e8f5e9
style C fill:#e3f2fd
Benefits of TDD
| Metric | Traditional development | TDD |
|---|---|---|
| Bug density | High | 60-80% lower |
| Regression issues | Frequent | Rare |
| Code quality | Unstable | Stable |
| Documentation value | Low | High (tests as docs) |
| Refactoring confidence | Low | High |
RED-GREEN-REFACTOR Explained
Phase 1: RED - Write a failing test
Goal: Write a test that fails and describes the expected behavior
Key principles:
1. Write only the test needed to describe one behavior
2. Do not think about implementation details
3. Tests should be concise and clear
4. Test names should describe intent
Example:
# Bad test - tests too many behaviors
def test_tag_creation_and_validation_and_slug():
tag = Tag(name="Test")
assert tag.name == "Test"
assert tag.slug == "test"
tag.save()
assert tag.id is not None
# Good test - one behavior only
def test_tag_creation_sets_name():
tag = Tag(name="Test")
assert tag.name == "Test"
def test_tag_creation_generates_slug():
tag = Tag(name="Test Tag")
assert tag.slug == "test-tag"
Phase 2: GREEN - Write the minimum code to pass the test
Goal: Make the test pass in the simplest way possible
Key principles:
1. Do not overdesign
2. You may write a "fake" implementation
3. YAGNI - do not build extra things
4. If the test is too complex, go back to RED and split it
Example:
# Test
def test_tag_slug_is_url_friendly():
tag = Tag(name="Hello World!")
assert tag.slug == "hello-world"
# ✅ The simplest implementation (it may be silly, but it works)
class Tag:
def __init__(self, name):
self.name = name
self.slug = "hello-world" # Hard-coded, but the test passes!
# The next test will force us to implement the real logic
Phase 3: REFACTOR - Improve the code
Goal: Improve code quality without changing behavior
Refactoring timing:
- Duplicate code appears
- Naming is unclear
- Functions are too long
- A class has too many responsibilities
- Common logic can be extracted
Refactoring checklist:
- [ ] Tests still pass
- [ ] No new warnings introduced
- [ ] Code is clearer
- [ ] No over-optimization
Superpowers TDD Skill Explained
When it triggers
The TDD skill triggers automatically in these scenarios:
- Implementing any feature - no matter how small
- Fixing bugs - write a reproduction test first
- Adding a new endpoint - write API tests first
Enforcement mechanism
flowchart TD
A[AI is about to write code] --> B{TDD check}
B -->|No test | C[❌ Block: write tests first]
B -->|Has tests | D[Allow implementation]
C --> E[Write tests]
E --> F[Run tests and see them fail]
F --> G{Tests fail?}
G -->|No | H[❌ Tests are wrong]
G -->|Yes | D
D --> I[Write implementation]
I --> J[Run tests]
J --> K{Tests pass?}
K -->|No | I
K -->|Yes | L[REFACTOR]
style C fill:#ffcccc
style H fill:#ffcccc
style L fill:#e8f5e9
TDD rules AI must follow
tdd:
rules:
- "Always write tests first"
- "Only write implementation after the tests fail"
- "Only write the minimum code needed to pass"
- "Refactor after it passes"
- "Delete any code written before the test"
enforcement:
- "If the user asks to write code directly, explain the TDD flow"
- "If the test is hard to write, the design is probably wrong"
- "If a test exceeds 20 lines, split it up"
Practical Example: Tag Validation
Requirement background
Add validation to the tag system:
- Tag name cannot be empty
- Tag name length must be 1-50 characters
- Tag name may contain only letters, numbers, spaces, and hyphens
Full TDD flow
Cycle 1: Empty tag name validation
RED - Write the test
# tests/test_tag_validation.py
def test_tag_name_cannot_be_empty():
"""Tag names cannot be empty"""
with pytest.raises(ValidationError) as exc_info:
Tag(name="")
assert "name" in str(exc_info.value)
assert "cannot be empty" in str(exc_info.value)
Run the test (expected to fail)
$ pytest tests/test_tag_validation.py::test_tag_name_cannot_be_empty
FAILED - Tag() got an unexpected keyword argument 'name'
GREEN - Write the implementation
# app/Models/Tag.py
class Tag:
def __init__(self, name: str):
if not name:
raise ValidationError("name cannot be empty")
self.name = name
Run the test (pass)
$ pytest tests/test_tag_validation.py::test_tag_name_cannot_be_empty
PASSED
REFACTOR - No refactor needed
Cycle 2: Length validation
RED - Write the test
def test_tag_name_max_length_is_50():
"""Tag name max length is 50"""
long_name = "a" * 51
with pytest.raises(ValidationError) as exc_info:
Tag(name=long_name)
assert "max length" in str(exc_info.value).lower()
def test_tag_name_exact_max_length_works():
"""Exactly 50 characters should work"""
name = "a" * 50
tag = Tag(name=name) # Should not raise an exception
assert tag.name == name
Run the test (fail)
$ pytest tests/test_tag_validation.py::test_tag_name_max_length_is_50
FAILED - No exception raised
GREEN - Write the implementation
class Tag:
MAX_NAME_LENGTH = 50
def __init__(self, name: str):
if not name:
raise ValidationError("name cannot be empty")
if len(name) > self.MAX_NAME_LENGTH:
raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
self.name = name
Run the test (pass)
$ pytest tests/test_tag_validation.py -v
test_tag_name_cannot_be_empty PASSED
test_tag_name_max_length_is_50 PASSED
test_tag_name_exact_max_length_works PASSED
REFACTOR - Extract validation methods
class Tag:
MAX_NAME_LENGTH = 50
def __init__(self, name: str):
self._validate_name(name)
self.name = name
def _validate_name(self, name: str) -> None:
if not name:
raise ValidationError("name cannot be empty")
if len(name) > self.MAX_NAME_LENGTH:
raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
Cycle 3: Character validation
RED - Write the test
def test_tag_name_allows_letters_numbers_spaces_hyphens():
"""Tag names may contain letters, numbers, spaces, and hyphens"""
valid_names = [
"python",
"machine learning",
"web-dev",
"AI and ML 2024",
"test-123",
]
for name in valid_names:
tag = Tag(name=name) # Should not raise an exception
assert tag.name == name
def test_tag_name_rejects_special_characters():
"""Tag names reject special characters"""
invalid_names = [
"python!",
"test@tag",
"hello#world",
"tag$name",
]
for name in invalid_names:
with pytest.raises(ValidationError) as exc_info:
Tag(name=name)
assert "invalid characters" in str(exc_info.value).lower()
Run the test (fail)
$ pytest tests/test_tag_validation.py::test_tag_name_rejects_special_characters
FAILED - No exception raised for 'python!'
GREEN - Write the implementation
import re
class Tag:
NAME_PATTERN = re.compile(r'^[a-zA-Z0-9 -]+$')
def _validate_name(self, name: str) -> None:
if not name:
raise ValidationError("name cannot be empty")
if len(name) > self.MAX_NAME_LENGTH:
raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
if not self.NAME_PATTERN.match(name):
raise ValidationError("name contains invalid characters")
Run the test (pass)
$ pytest tests/test_tag_validation.py -v
... (all tests pass)
REFACTOR - Final refactored code
import re
from typing import Final
class Tag:
"""Tag model class"""
MAX_NAME_LENGTH: Final[int] = 50
NAME_PATTERN: Final[re.Pattern] = re.compile(r'^[a-zA-Z0-9 -]+$')
def __init__(self, name: str):
self._validate_name(name)
self.name = name
self.slug = self._generate_slug(name)
def _validate_name(self, name: str) -> None:
"""Validate the tag name"""
self._validate_not_empty(name)
self._validate_length(name)
self._validate_characters(name)
def _validate_not_empty(self, name: str) -> None:
if not name:
raise ValidationError("name cannot be empty")
def _validate_length(self, name: str) -> None:
if len(name) > self.MAX_NAME_LENGTH:
raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
def _validate_characters(self, name: str) -> None:
if not self.NAME_PATTERN.match(name):
raise ValidationError("name contains invalid characters")
def _generate_slug(self, name: str) -> str:
"""Generate a URL-friendly slug"""
return name.lower().replace(' ', '-')
Reasonable Test Coverage Standards
Superpowers coverage requirements
test_coverage:
minimum:
models: 90% # Highest bar for models
services: 85% # Business logic
controllers: 80% # Controllers
utils: 95% # Utility functions
focus_on:
- "edge cases"
- "error handling"
- "business rules"
dont_worry_about:
- "getter/setter"
- "configuration classes"
- "plain data classes"
Test priority matrix
Importance
High ←─────────→ Low
┌─────────┬─────────┐
High │ Must test │ Should test │
│ (core logic) │ (supporting functionality) │
├─────────┼─────────┤
Low │ Optional │ No need │
│ (simple functions) │ (getter) │
└─────────┴─────────┘
TDD Anti-Patterns
Anti-pattern 1: Oversized tests
# Bad - the test does too much
def test_complete_tag_workflow():
tag = Tag(name="Test")
assert tag.name == "Test"
assert tag.slug == "test"
tag.save()
assert tag.id == 1
tag.update(name="Updated")
assert tag.name == "Updated"
tag.delete()
assert Tag.find(1) is None
# Good - one behavior per test
def test_tag_creation()
def test_slug_generation()
def test_tag_save()
def test_tag_update()
def test_tag_delete()
Anti-pattern 2: Test dependency
# Bad - tests depend on each other
def test_create_tag():
global tag_id
tag = Tag(name="Test")
tag.save()
tag_id = tag.id
def test_update_tag():
tag = Tag.find(tag_id) # Depends on the previous test!
tag.update(name="Updated")
# Good - each test is independent
def test_create_tag():
tag = Tag(name="Test")
tag.save()
assert tag.id is not None
def test_update_tag():
tag = Tag(name="Test")
tag.save()
tag.update(name="Updated")
assert tag.name == "Updated"
Anti-pattern 3: Too much mocking
# Bad - too many mocks, the test loses meaning
@mock.patch('database.Connection')
@mock.patch('cache.Redis')
@mock.patch('logger.Logger')
def test_tag_save(mock_logger, mock_redis, mock_db):
tag = Tag(name="Test")
tag.save()
# Good - only mock external dependencies when necessary
def test_tag_save():
# Use a real database (test-only)
tag = Tag(name="Test")
tag.save()
assert tag.id is not None
Anti-pattern 4: Testing private methods
# Bad - tests implementation details
def test_private_validate_name():
tag = Tag.__new__(Tag)
tag._validate_name("Test")
# Good - test through the public API
def test_invalid_name_raises_error():
with pytest.raises(ValidationError):
Tag(name="")
TDD Strategies for Common Scenarios
Scenario 1: Fixing a bug
Flow:
1. Write a test that reproduces the bug (it should fail)
2. Fix the bug
3. Make the test pass
4. Run regression tests to ensure nothing else broke
Example:
# Bug: slug generation breaks when the tag name contains spaces
# RED - reproduce the bug
def test_tag_slug_handles_spaces():
tag = Tag(name="hello world")
assert tag.slug == "hello-world" # Bug: the real output is "hello world"
# GREEN - fix it
def _generate_slug(self, name: str) -> str:
return name.lower().replace(' ', '-')
Scenario 2: Adding a new endpoint
Flow:
1. Write an API integration test (fail)
2. Implement the route and controller (pass)
3. Add business logic tests
4. Refactor
Scenario 3: Refactoring legacy code
Flow:
1. Add "characterization tests" for existing code
2. Make sure the tests pass (record current behavior)
3. Start refactoring
4. Keep the tests passing
Superpowers TDD Configuration
Configuration example
tdd:
# Strict mode (default)
enforcement: strict
# Test framework
framework: pytest # or unittest, jest, etc.
# Coverage requirements
coverage:
enabled: true
minimum: 85%
fail_below: true
# When to run tests
run_tests:
- "before_implementation"
- "after_implementation"
- "before_commit"
# Test generation
test_generation:
auto_generate: true
include_edge_cases: true
include_error_cases: true
Disabling TDD (not recommended)
It can be disabled temporarily in some situations:
tdd:
enabled: false # ⚠️ Prototype/demo only
# Or use rapid mode
tdd:
mode: rapid # Reduce the number of tests and cover only the main flow
Summary
TDD is a core discipline in Superpowers:
- RED - Write a failing test first
- GREEN - Write the minimum code to pass
- REFACTOR - Improve the code without changing behavior
Key points
- ✅ Tests always come first
- ✅ Iterate in small steps
- ✅ Tests are documentation
- ✅ Coverage is a result, not the goal
Benefits of TDD
- 📉 Bugs drop by 60-80%
- 📈 Refactoring confidence improves
- 📚 Living documentation is generated automatically
- 🧠 Better design, because you are forced to think through interfaces
Series navigation:
- ← Previous: Tutorial 3: writing-plans - Writing Implementable Technical Plans
- → Next: Tutorial 5: systematic-debugging - Systematic Debugging
- Back: Series Index