| name | test-driven-development |
| description | Use when implementing features or bug fixes where behavior should be specified before code. Applies best to new features, tricky logic, and high-correctness areas; avoid forcing it onto boilerplate, unstable spikes, or rapidly changing design work. |
Test-Driven Development
Overview
Use TDD when it improves clarity and confidence, not as a ritual to force onto every file.
Core loop:
- Red: write a failing test for the behavior you want.
- Green: write the minimum production code to make it pass.
- Refactor: clean up while keeping tests green.
Tests should describe behavior a user or caller expects, not the implementation details you happened to choose.
When to Use
Best fit:
- New features with clear expected behavior
- Bug fixes where you can reproduce the failure first
- Logic-heavy code that will change often
- High-risk or high-correctness areas
- Low-level components with stable behavior, such as parsers, allocators, or math
Usually skip or delay strict TDD for:
- Spikes and exploration when the problem is still unclear
- Static or boilerplate code
- Simple DTOs, models, and thin configuration wrappers
- UI design iteration where layout and presentation are still moving
- Domains where tests would lock in an API that is convenient for tests but awkward for real use
Pragmatic fallback when strict TDD is a poor fit:
- Use a few real end-to-end tests
- Add targeted unit tests for tricky logic
- Add regression tests after real bugs
- Prefer production rollouts and real-user metrics for behavior that tests cannot model well
Red-Green-Refactor
1. Red
Write one failing test for one behavior.
Requirements:
- Write the test before the implementation
- Keep it focused on a single observable behavior
- Use realistic inputs and outputs
- Prefer real collaborators over mocks
- If mocking is necessary, mock external systems, not your own business logic
Example:
test('retryOperation_givenTwoTransientFailures_returnsSuccessOnThirdAttempt', async () => {
let attempts = 0;
const result = await retryOperation(async () => {
attempts += 1;
if (attempts < 3) throw new Error('temporary failure');
return 'success';
});
expect(result).toBe('success');
expect(attempts).toBe(3);
});
2. Verify Red
Run the new test before writing production code.
Goal:
- Confirm it fails for the expected reason
- Catch false positives
- Prove the test is actually exercising missing behavior
If it passes immediately, the test is not proving what you think it is.
3. Green
Write the smallest amount of code needed to satisfy the behavior.
Guidelines:
- Do the minimum to pass
- Avoid speculative options and abstractions
- Keep the code simple
- Splitting code into smaller classes or helpers is fine if it improves testability and maintenance
4. Verify Green
Run the test again and confirm it passes before refactoring.
Also confirm:
- Nearby tests still pass
- The result matches intended behavior
- No warnings or obvious side effects were introduced
5. Refactor
Only refactor after the test is green.
Safe refactors include:
- Renaming
- Extracting helpers
- Removing duplication
- Simplifying structure
Keep tests green throughout.
Important limit:
TDD is not especially strong for large structural refactors when your test suite is tightly coupled to internals. If refactoring breaks many granular unit tests that assert structure instead of behavior, the suite is telling you the tests are too coupled.
What Good Tests Look Like
Good tests are:
- Fast
- Behavior-focused
- Easy to read
- Minimally mocked
- Named so failures read like documentation
Prefer:
- Observable outcomes
- Realistic values
- User-facing interactions for frontend tests
- Stable assertions that survive refactors
Avoid:
- Testing private implementation details
- Testing CSS position, colors, or markup structure unless that is the behavior
- Coupling test data to incidental implementation choices
- Stubbing your own services unless there is no practical alternative
Naming Tests
Test names matter because failures are part of the feedback loop.
Use names that tell you:
- What function or behavior failed
- Under what condition
- What outcome was expected
Good default shape:
functionName_givenCondition_returnsExpectation
Example:
getMovie_givenAvatar_returnsSimpleMovieDetails
Gherkin-style names are also good when they read more naturally.
Before writing a test, run through it in your head:
- What is the context?
- What does the user expect?
- What would count as success?
Mocking Guidance
Default stance:
- Minimize mocks
- Prefer real code paths
- Mock external boundaries such as paid APIs, third-party services, or infrastructure that is impractical to run in memory
Avoid:
- Mocking your own services by default
- Building tests around mock behavior
- Creating mocks without understanding which side effects the real code provides
If a unit test needs heavy mocking to be possible, pause and re-check the design. That often means the code is too coupled.
For more pitfalls, read testing-anti-patterns.md when adding mocks or test-only helpers.
Frontend Testing
For frontend work, test functionality rather than form.
Prefer assertions like:
- The button is visible
- The button can be clicked
- Clicking it triggers the expected behavior
- The user can submit the form and see the correct result
Avoid assertions like:
- Exact button position
- Specific colors
- Layout implementation details
TDD should be driven by user behavior, not by UI styling decisions.
Recommended Stack
Use these defaults unless the repository already has an established testing setup:
- Unit and integration tests: Vitest
- React component tests: React Testing Library with Vitest
- End-to-end browser tests: Playwright
When possible, run E2E tests against the production-like server path rather than a heavily mocked environment. For the highest-confidence checks, production rollouts and real metrics often catch issues tests miss.
Workflow for Bug Fixes
For a real bug:
- Reproduce it with a failing test.
- Verify the test fails for the expected reason.
- Fix the code with the smallest change that makes the test pass.
- Keep the regression test.
This is often more valuable than trying to force full TDD onto every surrounding file.
Coverage and Confidence
High coverage is not the goal.
Important reminders:
- 100% coverage does not guarantee reliability
- Low-value tests can game coverage metrics
- Systems with excellent coverage can still fail badly in production
Optimize for useful feedback, not vanity numbers.
Agent Workflow
When this skill is active:
- Decide whether strict TDD is appropriate for the task.
- If yes, write the failing test first and run it before implementation.
- Implement the minimum code to pass.
- Refactor only after green.
- If strict TDD is not the right tool, say so explicitly and use a pragmatic mix of E2E tests, targeted unit tests, and regression tests.
Do not claim TDD was followed unless the failing test existed and was run before the implementation.