Agent skill
react-tdd
Use when implementing React + Vite + TypeScript features or bugfixes with Vitest + React Testing Library tests, before writing implementation code.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/react-tdd
SKILL.md
React Test-Driven Development (TDD)
Overview
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
Quick Reference
| Step | Action | Verify |
|---|---|---|
| RED | Write one failing test | Fails for expected reason (missing component/function, not typo) |
| GREEN | Write minimal code to pass | This test + all others pass |
| REFACTOR | Clean up, no new behavior | All tests still green |
| Commit | After each green cycle | git commit with passing tests |
| Mock rule | Mock API boundaries only (MSW) | Zustand stores, domain logic, React components are real |
| Naming | it('should [behavior] when [condition]') |
Behavioral, no CanSet* |
| Assertions | Max 3 per test | Use it.each/describe.each for parameterized cases |
When to Use
Always: New features, bug fixes, refactoring, behavior changes.
Exceptions (ask your human partner): Throwaway prototypes, generated code, configuration files.
Thinking "skip TDD just this once"? Stop. That's rationalization.
The Iron Laws
1. NO IMPLEMENTATION CODE WITHOUT A FAILING TEST FIRST
2. NEVER add test-only methods/props to production components
3. MOCK BOUNDARY RULE (see below)
4. COMMIT after each green cycle
Write code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't
git stashit "for domain knowledge" - Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete —
git checkout .orrm, not stash
Mock Boundary Rule
Mock API boundaries with MSW. NEVER mock domain logic or React internals.
OK to mock (MSW): HTTP endpoints (http.get, http.post), WebSocket connections, external service APIs.
OK to mock (vi.mock): window.location, navigator.*, timers (vi.useFakeTimers), IntersectionObserver, ResizeObserver.
NEVER mock: Zustand stores, React components, custom hooks, utility functions, domain logic, event handlers.
// OK: MSW for API boundary
import { http, HttpResponse } from 'msw'
server.use(
http.get('/api/products/:id', () => {
return HttpResponse.json({ id: '1', name: 'Widget', price: 9.99 })
})
)
// NEVER: mocking Zustand store
vi.mock('../stores/useCartStore') // NO. Use real store.
// NEVER: mocking child components
vi.mock('./CartItem') // NO. Render real component.
What to Test
Test behavior, not structure. Ask: "If this test didn't exist, what bug could ship?" If "none" — delete it.
Priority hierarchy:
- User-visible behavior (click, type, see result)
- State transitions (store actions producing correct state)
- Integration boundaries (component + API via MSW)
- Edge cases (empty, null, boundary values)
NEVER test: Prop types, component renders without crashing, that a className exists, store getter values, hook return shapes.
No assertion-less tests: A test with zero assertions is always a bug. It passes silently and catches nothing.
"What Bugs Do These Tests Catch?" Table
REQUIRED for every test plan. Before writing tests, create this table:
| Test | Bug It Catches |
|---|---|
should show empty state when cart has no items |
Empty cart renders product list instead of empty message |
should update total when quantity changes |
Total doesn't recalculate on quantity change |
If you can't fill the "Bug It Catches" column — don't write the test. If most rows are low-value, reconsider whether you're testing the right behavioral surface.
Red-Green-Refactor
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="All tests\npass?", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
commit [label="COMMIT\ngit commit", shape=box, style=filled, fillcolor="#ffffcc"];
next [label="Next behavior", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="re-verify"];
refactor -> commit [label="all green"];
commit -> next;
next -> red;
}
RED - Write Failing Test
One minimal test showing desired behavior. Run it. Confirm it fails because the feature is missing (not typos/import errors).
import { render, screen } from '@testing-library/react'
import { CartPage } from './CartPage'
it('should show empty message when cart has no items', () => {
render(<CartPage />)
expect(screen.getByText(/your cart is empty/i)).toBeInTheDocument()
})
Requirements: One behavior per test. Clear name: it('should [behavior] when [condition]'). Real components, real stores.
GREEN - Minimal Code
Write simplest code to pass. Don't add features, refactor, or "improve" beyond the test.
REFACTOR
After green only: remove duplication, improve names, extract helpers. Keep all tests green.
COMMIT
After refactor and all tests green: git commit. Every green cycle gets a commit.
Naming Convention
it('should [expected behavior] when [condition/action]')
Examples:
it('should add item to cart when add button is clicked')it('should show error message when API returns 404')it('should disable submit button when form is invalid')
Assertion Rules
Max 3 assertions per test. More than 3? Split. Use it.each for parameterized tests.
Single logical assertion is fine even with multiple expect calls (e.g., checking name + price of one added item).
// GOOD: parameterized tests
it.each([
{ quantity: 1, price: 10, expected: 10 },
{ quantity: 3, price: 5.5, expected: 16.5 },
{ quantity: 0, price: 99, expected: 0 },
])('should calculate total as $expected when quantity=$quantity and price=$price',
({ quantity, price, expected }) => {
// ...test body
}
)
Custom Render Wrapper
REQUIRED: Create ONE shared render wrapper for tests needing providers. Reset Zustand stores in beforeEach — don't mock them.
REFERENCE: See references/test-utils-template.tsx for the complete custom render wrapper with providers.
MSW Patterns
REFERENCE: See references/msw-patterns.md for MSW setup and per-test override patterns.
Self-Review Questions
Before marking any test as done, ask yourself:
- "Would this test catch a real bug?" — If no, delete it.
- "Am I testing behavior or implementation details?" — If testing that a mock was called, stop.
- "Did I watch this test fail first?" — If it passed immediately, it proves nothing.
- "Is my test name a complete sentence?" —
should [x] when [y].
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing — you never saw it catch the bug. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested in browser" | Ad-hoc != systematic. No record, can't re-run. |
| "Deleting X hours of code is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Git stash it for domain knowledge" | Stash = reference with extra steps. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "TDD is for new work, not retroactive" | If code exists without tests, delete it and do TDD. No exceptions. |
| "Personal time matters more" | Stop working. Resume with TDD tomorrow. Don't commit untested code. |
| "Just the critical tests for now" | Partial testing = false confidence. TDD or don't commit. |
| "Being pragmatic, not dogmatic" | Pragmatic = test-first. Shortcuts = debt. |
Red Flags - STOP and Start Over
These flags indicate TDD discipline violations (process problems). For test quality anti-pattern symptoms, see testing-anti-patterns.md Red Flags.
- Code before test / test after implementation
- Test passes immediately (never saw it fail)
- Can't explain why test failed
- Rationalizing "just this once" or "this is different because..."
- Testing that a className/prop exists (
CanSetPropertiesequivalent) - Testing that a mock was called instead of real behavior
- More than 3 unrelated assertions / no
it.eachfor multiple inputs - Test with zero assertions
vi.mockon Zustand stores, components, or custom hooks- "Already spent X hours, deleting is wasteful"
- Committing without tests "for now"
All of these mean: Delete code. Start over with TDD.
Testing Anti-Patterns
REFERENCE: See testing-anti-patterns.md for the full list of anti-patterns with examples and gate functions.
When Stuck
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for UI. Write assertion first: expect(screen.getByText('...')).toBeInTheDocument() |
| Test too complicated | Component too complicated. Extract smaller component. |
| Must mock everything | Code too coupled. Lift state up, use dependency injection via props/context. |
| Test setup huge | Extract to custom render wrapper. Still complex? Simplify component. |
| Async timing issues | Use waitFor, findBy* queries. Never setTimeout in tests. |
| Zustand store hard to test | Test through component rendering, not getState() directly. |
Final Rule
Implementation code -> test exists and failed first
Otherwise -> not TDD
No exceptions without your human partner's permission.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?