Vitest Standards
Philosophy
- •A test exists to prevent a real regression — if it can't break when behavior breaks, delete it
- •Test behavior and outcomes, NEVER implementation details
- •If a test breaks on refactor without behavior change — it's a bad test, delete it
- •Less tests with high value > many tests with low value
- •Every test must answer: "What user-visible or business-critical behavior does this protect?"
What to Test (Worth It)
- •Business logic / pure functions: calculations, transformations, validation rules, state machines
- •Edge cases: empty inputs, boundary values, null/undefined, overflow, unicode, concurrent calls
- •Error paths: what happens when things fail — error messages, fallback behavior, recovery
- •Integration points: API call → state update → correct output (mock the network, test the flow)
- •Complex conditionals: if a function has 3+ branches, each branch deserves a test
- •Regressions: every bug fix gets a test that would have caught the bug
- •Custom hooks: state transitions, side effect ordering, cleanup behavior
- •User interactions (component tests): click → outcome, submit → validation, type → filtered results
What NOT to Test (Waste of Time)
- •NEVER test that a CSS/Tailwind class is applied — that's testing the template engine
- •NEVER test that a component renders without crashing — if it doesn't render, you'll know
- •NEVER test prop forwarding (
<Child title={title} />then assert Child got title) - •NEVER test static text content ("renders the correct heading")
- •NEVER test that a library works (React renders, router routes, i18n translates)
- •NEVER test type correctness — that's TypeScript's job
- •NEVER test 1:1 mirrors of implementation (copy-pasting the function logic into the test)
- •NEVER test trivial getters/setters or simple wrappers
- •NEVER write a test just to increase coverage — coverage is a side effect, not a goal
Decision Framework
Before writing any test, pass ALL 4 gates:
- •Can this break? If the code is trivial or type-safe, skip it
- •Will this catch a real bug? If only a copy-paste of the implementation would fail, skip it
- •Is this the right testing level? Unit for logic, integration for flows, E2E for critical paths
- •Is this resilient to refactors? If renaming a variable or moving a div breaks it, skip it
Component Testing (React Testing Library)
- •Query by role, label, placeholder, text — NEVER by test-id unless no accessible alternative
- •
test-idis a last resort, not a default — if you need it, the component likely has an a11y issue - •Test user workflows: "user types email → clicks submit → sees success message"
- •NEVER assert on DOM structure, element count, or class names
- •
userEventoverfireEvent— always (closer to real user behavior) - •NEVER snapshot test components — they break on every change, nobody reads the diff
- •Mock API responses at the network level (
msw) — NEVER mock React hooks directly
Mocking
- •Mock ONLY external boundaries: network, filesystem, timers, randomness, dates
- •Pure functions and business logic stay real — NEVER mock them
- •ALWAYS clean up:
vi.restoreAllMocks()inafterEach - •ALWAYS
vi.useRealTimers()inafterEachwhen using fake timers - •
vi.hoisted()for shared module-level mocks - •Prefer dependency injection over
vi.mock()— easier to test, easier to read - •If you mock more than 2 things in a test, the unit is too coupled — refactor the code
- •3-mock threshold: If a test requires 3+ mocks, add
// WARNING: over-mocked — code coupling issue, consider refactoringand flag to team lead
Assertions
- •Specific assertions:
toHaveBeenCalledWith(expected)— NEVERtoHaveBeenCalled()alone - •
toEqualfor deep object comparison,toBefor primitives/references - •
toMatchInlineSnapshot()for complex outputs — NEVER external.snapfiles - •One logical assertion per test — multiple
expectare fine if testing the same behavior - •
expect.objectContaining()/expect.arrayContaining()to avoid brittle exact matches - •NEVER assert on the entire response object when you only care about 2 fields
Structure
- •
describeblocks group by behavior/feature, not by method name - •Test names describe the scenario and expected outcome:
it('returns empty array when filter matches nothing') - •AAA pattern: Arrange → Act → Assert — separated by blank lines
- •Factory functions (
createUser(),buildOrder()) for test data — NEVER inline object literals everywhere - •Shared setup in
beforeEach— NEVER in module scope (test isolation) - •Keep tests flat: max 2 levels of
describenesting — deeper = bad test organization
Async
- •ALWAYS
awaitasync assertions — un-awaited expect silently passes - •
vi.waitFor()for eventually-consistent behavior (DOM updates, debounced calls) - •
vi.advanceTimersByTime(ms)overvi.runAllTimers()for precise control - •Test both resolve and reject paths for async functions
- •Clean up in
afterEach: cancel pending timers, abort controllers, unmount components
Test File Organization
- •Colocate test files next to source:
user-service.ts→user-service.test.ts - •One test file per source file — split if test file exceeds 300 lines
- •300-line split rule: When splitting, group by behavior. Shared factories and helpers go to
__test-utils__/directories, not genericutils/
TDD Template
Reference structure for strict TDD workflows:
typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
// Factory functions at top
const createTestData = (overrides = {}) => ({
// sensible defaults representing valid state
...overrides,
})
describe('<Feature> — <behavior group>', () => {
beforeEach(() => {
// shared setup — reset state, seed mocks
})
afterEach(() => {
vi.restoreAllMocks()
})
it('<scenario> — <expected outcome>', () => {
// Arrange
const input = createTestData()
// Act
const result = functionUnderTest(input)
// Assert
expect(result).toEqual(expected)
})
describe('edge cases', () => {
it('handles empty input', () => { /* ... */ })
it('handles null/undefined', () => { /* ... */ })
it('handles boundary values', () => { /* ... */ })
})
describe('error paths', () => {
it('throws on invalid input', () => { /* ... */ })
it('returns fallback on failure', () => { /* ... */ })
})
})
Anti-Patterns — NEVER Do These
- •NEVER test implementation details — no internal state, private methods, CSS classes, DOM structure
- •NEVER use snapshot tests for components — they break on every change, nobody reads the diff
- •NEVER use
getByTestIdas default query strategy — it masks a11y issues - •NEVER leave
vi.mock()withoutvi.restoreAllMocks()inafterEach - •NEVER write tests just for coverage numbers — every test must pass the 4-gate decision framework
- •NEVER use
anyin test types — use test-specific types orunknownwith type guards - •NEVER use external
.snapfiles — inline snapshots only (toMatchInlineSnapshot()) - •NEVER share mutable state between tests — each test must be independent
- •NEVER use
fireEventwhenuserEventis available - •NEVER mock pure functions or internal modules