TDD Workflow
When to Use
Activate this skill when:
- •The user explicitly requests TDD, test-first, or red-green-refactor
- •Implementing new functions, methods, endpoints, or components where test-first is valuable
- •Fixing bugs where a regression test should be written first
- •The user says "write the test first", "TDD this", or "red-green-refactor"
Do NOT use this skill for:
- •Configuration files, environment setup, or static content
- •One-line fixes or trivial changes
- •Exploratory prototyping or proof-of-concept code
- •Code that cannot be meaningfully tested in isolation
- •Testing pattern details (use
pytest-patternsorreact-testing-patternsfor HOW to write tests)
Instructions
The TDD Cycle
┌─────────────────────────────────────────────────────┐ │ │ │ ┌─────┐ ┌───────┐ ┌──────────┐ │ │ │ RED │ ──→ │ GREEN │ ──→ │ REFACTOR │ ──→ ... │ │ └─────┘ └───────┘ └──────────┘ │ │ │ │ Write ONE Write MINIMUM Clean up code │ │ failing code to make while ALL tests │ │ test it pass stay GREEN │ │ │ └─────────────────────────────────────────────────────┘
Phase 1: RED — Write a Failing Test
- •Write exactly ONE test that describes the expected behavior
- •The test must be specific: test one behavior, not multiple
- •Run the test suite and confirm the new test FAILS
- •The failure message should clearly describe what is missing
Backend (pytest):
pytest tests/unit/test_user_service.py::test_create_user_returns_user -x # Expected: FAILED (function/class does not exist yet)
Frontend (Vitest):
npx vitest run src/hooks/useAuth.test.ts --reporter=verbose # Expected: FAILED (hook/component does not exist yet)
Rules for RED phase:
- •Write the simplest test that expresses the requirement
- •The test should fail for the RIGHT reason (missing implementation, not syntax error)
- •Don't write more than one failing test at a time
- •Import from the location where the code WILL live (even though it doesn't exist yet)
Phase 2: GREEN — Make It Pass
- •Write the MINIMUM code to make the failing test pass
- •Do NOT add extra functionality, error handling, or edge cases
- •It's okay to hardcode values or use simple implementations
- •Run the test suite and confirm ALL tests pass (not just the new one)
# Backend pytest tests/unit/test_user_service.py -x # Frontend npx vitest run src/hooks/useAuth.test.ts
Rules for GREEN phase:
- •Minimum code means minimum — if a constant satisfies the test, use a constant
- •Do not add code that no test requires
- •Do not refactor during this phase
- •Do not write additional tests during this phase
- •If tests pass, move to REFACTOR
Phase 3: REFACTOR — Clean Up
- •Improve code quality while keeping all tests green
- •Remove duplication (DRY)
- •Improve naming and readability
- •Extract functions or classes if needed
- •Run tests after EVERY change — they must stay green
# After each refactoring change pytest tests/ -x # Must pass npx vitest run # Must pass
Rules for REFACTOR phase:
- •Every change must keep tests green
- •Refactor both production code AND test code
- •Do NOT add new functionality (that requires a new RED phase)
- •If you break a test, undo the refactoring immediately
Phase 4: COMMIT
After a successful REFACTOR phase:
- •Stage all changes (test + implementation)
- •Commit with a descriptive message
- •Return to Phase 1 (RED) for the next behavior
Strict TDD Rules
These rules are non-negotiable when this skill is active:
- •NEVER write production code without a failing test first
- •NEVER write more than one failing test at a time
- •NEVER add functionality that no test requires
- •ALWAYS run tests after every change
- •ALWAYS commit after each successful GREEN-REFACTOR cycle
- •ALWAYS keep the RED-GREEN-REFACTOR cycle short (minutes, not hours)
Backend TDD Flow (pytest)
1. Write test: tests/unit/test_user_service.py::test_create_user_returns_user 2. Run: pytest tests/unit/test_user_service.py::test_create_user_returns_user -x 3. See: FAILED - ImportError or AssertionError 4. Implement: app/services/user_service.py (minimum code) 5. Run: pytest tests/unit/test_user_service.py -x 6. See: PASSED 7. Refactor: Clean up, run tests again 8. Commit: "Add UserService.create_user" 9. Next test: test_create_user_rejects_duplicate_email
Frontend TDD Flow (Testing Library)
1. Write test: src/components/UserCard.test.tsx::renders user name 2. Run: npx vitest run src/components/UserCard.test.tsx 3. See: FAILED - module not found 4. Implement: src/components/UserCard.tsx (minimum code) 5. Run: npx vitest run src/components/UserCard.test.tsx 6. See: PASSED 7. Refactor: Clean up, run tests again 8. Commit: "Add UserCard component" 9. Next test: calls onEdit when button clicked
Bug Fix TDD Flow
When fixing a bug, always start with a failing test that reproduces the bug:
1. Reproduce: Understand the bug and its trigger condition 2. Write test: Test that exercises the exact scenario that causes the bug 3. Run: Confirm FAILED (the test reproduces the bug) 4. Fix: Implement the minimum fix 5. Run: Confirm PASSED (bug is fixed) 6. Refactor: Clean up if needed 7. Commit: "Fix: [describe the bug]"
This guarantees the bug cannot regress — the test will catch it.
Examples
TDD: UserService.create_user (3 Cycles)
Cycle 1 — RED: Test that create_user returns a user
async def test_create_user_returns_user(db_session):
service = UserService(db_session)
user = await service.create_user(UserCreate(email="a@b.com", password="12345678", display_name="A"))
assert user.email == "a@b.com"
assert user.id is not None
GREEN: Implement UserService.create_user with basic logic.
REFACTOR: Extract password hashing. Commit.
Cycle 2 — RED: Test that duplicate email raises error
async def test_create_user_rejects_duplicate_email(db_session):
service = UserService(db_session)
await service.create_user(UserCreate(email="a@b.com", password="12345678", display_name="A"))
with pytest.raises(ConflictError):
await service.create_user(UserCreate(email="a@b.com", password="87654321", display_name="B"))
GREEN: Add duplicate check before insert. REFACTOR: Clean up. Commit.
Cycle 3 — RED: Test that password is hashed
async def test_create_user_hashes_password(db_session):
service = UserService(db_session)
user = await service.create_user(UserCreate(email="a@b.com", password="12345678", display_name="A"))
assert user.hashed_password != "12345678"
assert verify_password("12345678", user.hashed_password)
GREEN: Already passing from cycle 1 refactor? Then this test is a verification, not a RED. Write a test for a NEW behavior instead.
Edge Cases
- •
When to skip TDD: Configuration files (
.env,tsconfig.json), static content, auto-generated code (Alembic migrations), one-off scripts, and exploratory prototyping. - •
TDD with external dependencies: Mock at the boundary. If testing a service that calls an external API, mock the API client, not the HTTP library. Test the service's behavior, not the mock.
- •
Large features: Break the feature into small, testable behaviors. Each behavior gets its own RED-GREEN-REFACTOR cycle. The sum of all cycles implements the full feature.
- •
Refactoring existing code without tests: First write tests for the existing behavior (characterization tests). Then refactor with those tests as a safety net. This is not strict TDD but is a valid use of the test-first mindset.
- •
Pair with pattern skills: This skill defines the WORKFLOW (when to write tests vs code). Use
pytest-patternsorreact-testing-patternsfor the PATTERNS (how to structure tests, which assertions to use).