Test-Driven Development (TDD)
When to Use This Skill
Read when: Writing implementation code with tests.
Referenced by: Implementation Agent (.agents/agents/implementation.md)
🎯 Critical Rule: ONE Test at a Time 🚨
⚠️ MANDATORY - NON-NEGOTIABLE ⚠️
Work on ONE test at a time. Finish it completely before moving to the next test.
This is the MOST IMPORTANT rule in TDD.
The Cycle (ONE at a time)
🔴 ONE TEST: Write → Verify FAILS → Implement → Verify PASSES → Refactor
↓
🔴 NEXT TEST: Write → Verify FAILS → Implement → Verify PASSES → Refactor
↓
🔴 NEXT TEST: Write → Verify FAILS → Implement → Verify PASSES → Refactor
❌ NEVER Do This (FORBIDDEN)
- ❌ Write multiple tests at once
- ❌ Generate test file with all tests
- ❌ Write implementation for multiple tests simultaneously
- ❌ Skip ahead to other tests before current one passes
- ❌ Plan out 10 tests and write them all
- ❌ Copy-paste test template with TODO comments
✅ ALWAYS Do This (MANDATORY)
- ✅ Write ONE test (just one!)
- ✅ Verify it FAILS (red)
- ✅ Implement minimum code to pass THAT test
- ✅ Verify it PASSES (green)
- ✅ Refactor if needed (while staying green)
- ✅ ONLY THEN move to NEXT test
- ✅ Repeat: ONE test at a time
Why ONE at a time:
- ✅ Focus: Single behavior, no distractions
- ✅ Validation: Each test proves the last code works
- ✅ Debugging: Know exactly what broke
- ✅ Progress: Concrete progress with each passing test
- ✅ Design: Better API design from incremental feedback
- ✅ Safety: Tests catch regressions immediately
📖 The TDD Workflow
Step 1: Write the Test FIRST
Before any implementation code:
/// WHY: Token must expire at midnight (edge case from security review)
/// WHAT: Token with midnight expiry should be treated as expired
#[test]
fn test_token_expires_at_midnight() {
let token = create_token_with_expiry("2024-01-15T00:00:00Z");
assert!(is_expired(&token));
}
Step 2: Verify Test FAILS
Run test to confirm it fails (proves test is valid):
cargo test test_token_expires_at_midnight
# Should fail: function doesn't exist yet
Step 3: Implement Minimum Code
Write just enough code to pass THIS test:
pub fn is_expired(token: &Token) -> bool {
let now = Utc::now();
token.expires_at <= now
}
Step 4: Verify Test PASSES
cargo test test_token_expires_at_midnight
# Should pass: implementation now correct
Step 5: Refactor If Needed
Improve code structure (tests stay green):
pub fn is_expired(token: &Token) -> bool {
is_past_expiry(token.expires_at)
}
Step 6: Move to NEXT Test
/// WHY: Tokens with null expiry should never expire (spec requirement)
/// WHAT: Token without expiry field should be treated as valid
#[test]
fn test_token_without_expiry_never_expires() {
let token = create_token_without_expiry();
assert!(!is_expired(&token));
}
📖 Complete examples: tdd-workflow-examples.md - Rust, TypeScript, Python
📝 Test Documentation (MANDATORY)
Every test MUST have WHY and WHAT:
/// WHY: <business reason, requirement, or bug>
/// WHAT: <specific behavior being tested>
#[test]
fn test_name() { }
Why this matters:
- Future developers understand purpose
- Links to requirements/specs
- Documents business rules
- Makes tests maintainable
📖 Complete guide: test-documentation.md - Templates and examples
✅ Valid Test Usage
Good tests:
- Descriptive name - Explains what is being tested
- WHY/WHAT docs - Business context and behavior
- Specific assertions - Check exact expected values
- One behavior - Tests single requirement/case
- Independent - Doesn't depend on other tests
/// WHY: Security requirement from audit
/// WHAT: Admin role should have all permissions
#[test]
fn test_admin_has_all_permissions() {
let admin = create_admin_user();
assert!(admin.has_permission(Permission::Read));
assert!(admin.has_permission(Permission::Write));
assert!(admin.has_permission(Permission::Delete));
}
❌ Invalid Test Usage
Bad tests:
- No documentation - No WHY/WHAT
- Vague assertions -
assert!(result)without checking what - Multiple behaviors - Tests 5 things in one test
- Muted variables -
let _result = ...without assertions - Empty body -
#[test] fn test_something() { }
// ❌ BAD - No docs, vague assertion
#[test]
fn test_user() {
let user = User::new();
assert!(user.id > 0); // What are we really testing?
}
🔍 Common Patterns (Read When Needed)
When you need to:
- Build feature with multiple requirements →
tdd-patterns.md#feature-with-multiple-requirements - Test edge cases →
tdd-patterns.md#edge-case-testing - Refactor safely →
tdd-patterns.md#refactoring-with-tdd-safety - Build complex algorithms →
tdd-patterns.md#building-complex-algorithms - Test different data scenarios →
tdd-patterns.md#data-driven-development - Build error handling →
tdd-patterns.md#error-handling-development
⚠️ Common Pitfalls
Avoid these mistakes:
- Writing all tests first → Write ONE test, implement, then next
- Skipping failure verification → Always verify test fails first
- Over-implementing → Write minimum code to pass current test
- Poor test names → Use descriptive names explaining behavior
- Missing WHY/WHAT → Always document business context
- Testing implementation → Test behavior, not internal details
- Dependent tests → Each test should run independently
🎯 TDD Benefits
Why TDD works:
- ✅ Better design - Writing tests first improves API design
- ✅ Safety net - Tests catch regressions immediately
- ✅ Documentation - Tests document expected behavior
- ✅ Confidence - Know code works as expected
- ✅ Incremental - Small steps prevent overwhelm
- ✅ Focus - One test = one requirement at a time
📋 TDD Checklist
Every test should have:
- Written BEFORE implementation
- WHY/WHAT documentation
- Verified it FAILS first
- Minimum implementation to pass
- Verified it PASSES
- Refactored if needed
- One test finished before starting next
🔗 Related Skills
- Rust Testing Excellence - Rust-specific testing patterns
- Implementation Practices - General implementation guidelines