test-first-thinking
Test-First Thinking
Overview
Test-first thinking is a design discipline that requires thinking about features, testability, and maintainability BEFORE writing implementation code. This is not strict TDD (Test-Driven Development), but rather a mental model that ensures you design simple, testable interfaces from the start.
Core Principle: If you can't easily describe what tests you'd write, your design may be too complex.
When to Use This Skill
Explicit Triggers
- "Think about tests first"
- "Design for testability"
- "What tests do I need?"
- "Use test-first approach"
- "TDD thinking"
- "How should I test this?"
Implicit Triggers
- Before creating a new class or method
- Before refactoring existing code
- When starting a new feature implementation
- When reviewing code that lacks tests
- When design feels overly complex
Debugging Triggers
- Tests are difficult to write for existing code
- Code requires extensive mocking to test
- Implementation has grown too complex
- Edge cases keep surfacing after deployment
What This Skill Does
This skill guides you through a pre-implementation checklist that ensures:
- Feature enumeration - List all expected behaviors before coding
- Simplicity check - Consider maintainability and complexity
- Test identification - Know what tests validate each behavior
- Interface design - Create signatures that make testing easy
- Edge case awareness - Think through error conditions upfront
The Test-First Checklist
Run through this checklist BEFORE writing implementation code:
1. Enumerate Features and Behaviors
Ask yourself:
- What should this class/method do?
- What are the expected inputs and outputs?
- What transformations or side effects occur?
Example:
# Before implementing UserRegistration class, list features:
# 1. Validate email format
# 2. Check if email already exists
# 3. Hash password securely
# 4. Store user in database
# 5. Send confirmation email
# 6. Return success/failure result
2. Consider Edge Cases and Error Conditions
Ask yourself:
- What can go wrong?
- How should errors be handled?
- What are the boundary conditions?
Example:
# Edge cases for UserRegistration:
# - Invalid email format
# - Duplicate email
# - Weak password
# - Database connection failure
# - Email service unavailable
# - Null/empty inputs
3. Identify Required Tests
Ask yourself:
- What test cases validate each feature?
- How do I verify error handling?
- What mocks/fixtures are needed?
Example:
# Tests needed for UserRegistration:
# - test_valid_registration_succeeds()
# - test_invalid_email_raises_validation_error()
# - test_duplicate_email_returns_failure()
# - test_weak_password_raises_validation_error()
# - test_database_failure_returns_failure()
# - test_email_service_failure_logs_warning()
# - test_null_inputs_raise_value_error()
4. Design for Testability
Ask yourself:
- Does this interface make testing easy?
- Can I test without complex mocking?
- Are dependencies explicit and injectable?
- Is the function pure (no hidden side effects)?
Good Design (Testable):
def register_user(
email: str,
password: str,
user_repo: UserRepository,
email_service: EmailService
) -> Result[User, RegistrationError]:
"""Register new user with explicit dependencies."""
# Dependencies are injected - easy to mock
# Returns Result type - easy to test both paths
# Pure function - predictable behavior
Bad Design (Hard to Test):
def register_user(email: str, password: str) -> None:
"""Register new user."""
# Hidden dependency on global database connection
# Hidden dependency on email service
# No return value - can't verify success
# Side effects make testing difficult
5. Look at Existing Tests First
When editing existing code:
- Read the test file BEFORE modifying implementation
- Existing tests show what features the code should have
- If tests are missing, write them first
- If tests are hard to understand, the code is likely too complex
Example:
# Before editing src/auth/registration.py:
# 1. Read tests/unit/auth/test_registration.py
# 2. Understand what behaviors are tested
# 3. Identify what's NOT tested (gaps)
# 4. Add tests for new behavior
# 5. THEN modify implementation
6. Implement
Only after completing steps 1-5:
- Write the implementation
- Run tests continuously as you code
- Refactor based on test feedback
- Add tests if new edge cases emerge
Quick Reference: Red Flags
Stop and reconsider if you encounter:
- "I'll write tests later" - Write tests now or redesign
- "This needs extensive mocking" - Dependencies may be too coupled
- "I can't describe what tests I'd write" - Design is too complex
- "Tests would be too complicated" - Implementation is too complicated
- "This is hard to test" - This is hard to maintain
- "I need to mock everything" - Too many dependencies
- "Tests keep breaking" - Implementation is too fragile
Benefits
| Aspect | Before Test-First Thinking | After Test-First Thinking |
|---|---|---|
| Design Complexity | Grows organically, becomes tangled | Kept simple by testability constraint |
| Test Coverage | Written after (if at all), incomplete | Designed in from start, comprehensive |
| Edge Cases | Discovered in production | Identified during design |
| Debugging Time | High - complex interactions | Low - isolated, testable units |
| Refactoring Confidence | Low - fear of breaking things | High - tests verify behavior |
| Maintenance Cost | High - difficult to change | Low - clear contracts and tests |
Integration with Quality Gates
This skill supports:
- quality-run-quality-gates - Ensures tests exist before marking complete
- quality-capture-baseline - Requires test coverage metrics
- quality-detect-regressions - Verifies tests pass consistently
- test-debug-failures - Makes test failures easier to diagnose
Expected Outcomes
Success
Before implementing PaymentProcessor class:
Features enumerated:
✅ Process credit card payment
✅ Validate payment amount
✅ Handle payment gateway response
✅ Store transaction record
✅ Send receipt email
Edge cases identified:
✅ Invalid card number
✅ Insufficient funds
✅ Gateway timeout
✅ Network failure
✅ Duplicate transaction
Tests identified:
✅ test_valid_payment_succeeds()
✅ test_invalid_card_raises_error()
✅ test_insufficient_funds_returns_failure()
✅ test_gateway_timeout_retries()
✅ test_duplicate_transaction_prevented()
Interface designed:
✅ Dependencies injected (gateway, transaction_repo)
✅ Returns Result type for error handling
✅ Pure function - no hidden state
✅ Easy to mock gateway for testing
Ready to implement with confidence!
Failure (Redesign Needed)
Before implementing ReportGenerator class:
Attempted to list features:
❌ "Generate reports" - too vague
❌ "Process data" - what data? how?
❌ Multiple responsibilities identified
❌ Can't describe specific behaviors
Attempted to identify tests:
❌ "Test that it works" - not specific enough
❌ Would need to mock 15+ dependencies
❌ No clear success/failure paths
❌ Can't isolate behaviors for testing
Red flags:
❌ Design too complex
❌ Unclear responsibilities
❌ Too many dependencies
❌ Not testable in current form
Action: Break into smaller, focused classes:
- ReportDataFetcher (single responsibility)
- ReportFormatter (single responsibility)
- ReportExporter (single responsibility)
Retry test-first thinking for each class individually.
Notes
- This is NOT strict TDD - You don't have to write tests first, but you must THINK about tests first
- Mental model matters - The discipline of considering testability improves design
- Start simple - If you can't explain it simply, you don't understand it well enough
- Tests reveal design flaws - Hard to test = hard to maintain
- Iterate - If tests are difficult, redesign the interface
- Use existing tests as documentation - They show what the code should do
- Testability IS maintainability - They're the same thing
Supporting Files
This skill is intentionally minimal - it's a thinking discipline, not a complex workflow. No additional scripts or references are needed.