testing-vitest
Skill1 starsChanged 5 months agoMITView on GitHub
Use when writing, reviewing, or improving tests for React projects using Vitest and TypeScript. Triggers on "write tests for this component", "add test coverage", "review this test file", "fix a failing test", "how should I test this hook/utility/service", "what's wrong with my test?", or any time a React + Vitest + TypeScript project needs test coverage. Covers component tests, hooks, utilities, and async logic.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- luigi-dv/react-ts-vitetheir own version, first seen Oct 20, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- luigi-dv/react-ts-vite★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository10 other files it gives its agents.
- crafting-commitsSkill
- fetching-ticketsSkill
- generating-tasksSkill
- implementing-tasksSkill
- picking-up-taskSkill
- planning-from-specSkill
- receiving-plan-reviewSkill
- reviewing-codeSkill
- reviewing-planSkill
- testing-pytestSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
21 rules. About 0 are common, 8 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 22
Before writing any tests, understand the code under test.
- Distinctive: line 32
Prefer sociable tests: Only mock what you can't control — network, timers, the filesystem, third-party APIs.
- Distinctive: line 68
Do NOT use "should": Write test names as declarative statements of fact:
- Very specific: line 96
Use ComponentName.test.tsx or useHookName.test.ts co-located next to the source file.
- Distinctive: line 103
Every test follows Arrange → Act → Assert with visual separation:
- Distinctive: line 122
Keep phases distinct. Don't interleave assertions with actions.
- Very specific: line 145
Use @testing-library/react with Vitest.
- Distinctive: line 156
Never query by class name, CSS selector, or internal component name.
- Very specific: line 181
Prefer userEvent over fireEvent — it simulates real browser interactions (pointer events, focus, etc.) rather than dispatching a single …
- Very specific: line 188
Use renderHook from @testing-library/react.
- Very specific: line 213
Always await async assertions.
- Very specific: line 234
Use findBy* (not waitFor + getBy*) when waiting for something to appear in the DOM.
- Very specific: line 235
Use waitForElementToBeRemoved when waiting for something to disappear.
- Very specific: line 286
Never use
as anyin test code: If it won't type-check, the test data doesn't match the real type — fix the data, not the type. - Very specific: line 306
Don't cast partial objects with as User.
- Very specific: line 332
Use fake timers when the unit's behavior depends on setTimeout, setInterval, Date, or debounce/throttle logic.
- Very specific: line 360
When fake timers and promises interact (e.g., a debounced fetch), use vi.advanceTimersByTimeAsync(ms) instead — it flushes microtasks …
- Very specific: line 368
Use test.each or test.for (Vitest-native) for repetitive cases over a data table rather than copy-pasting tests:
- Very specific: line 388
Use snapshots sparingly. They catch regressions but don't communicate intent.
- Distinctive: line 395
When you do use snapshots, keep them small and pair them with explicit assertions:
- Distinctive: line 411
Don't test internals: If a test breaks when you rename a private variable or extract a helper function, it's testing implementation detail.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
testing-vitest
You are an expert at writing unit tests for React projects using Vitest and TypeScript.
Step 1 — Understand the code under test
Before writing any tests, understand the code under test. Read the source file fully. Identify: what it exports, what its public API is, what side effects it has, and what dependencies it reaches for (network, timers, context, etc.).
Core Philosophy
Test behavior, not implementation. Tests are readable, maintainable, trustworthy, and test behavior — not implementation details. The invariant: if you refactor the internals without changing behavior, every test should still pass. If a test breaks on an internal rename, it was testing the wrong thing.
Prefer sociable tests. Only mock what you can't control — network, timers, the filesystem, third-party APIs. Real React state, real hook interactions, real utility calls are fine and give more confidence than a chain of mocks.
DAMP over DRY. A test should be self-contained. A reader shouldn't have to scroll
up through three beforeEach blocks to understand what a test is setting up.
Favor inline setup or named factory functions called explicitly per test.
One behavior per test, not one assertion per test. A single user action may produce several observable effects; asserting them all in one test is fine. What's not fine is testing two unrelated behaviors in one test.
Naming Conventions
describe blocks
Name after the unit under test — the component, function, or hook name:
describe('PasswordInput', () => { ... })
describe('useDebounce', () => { ... })
describe('formatCurrency', () => { ... })Group sub-behaviors with nested describes only when there are meaningfully distinct scenarios and nesting won't exceed two levels:
describe('useAuth', () => {
describe('when the session has expired', () => { ... })
describe('when the user is authenticated', () => { ... })
})it/test names — the user's preference
Do NOT use "should". Write test names as declarative statements of fact:
// ✅ Correct
it('returns null for an empty array', ...)
it('renders a disabled button when loading is true', ...)
it('calls onSubmit with the trimmed input value', ...)
it('throws an error when the token is missing', ...)
it('applies the error class when validation fails', ...)
// ❌ Avoid
it('should return null for an empty array', ...)
it('should render a disabled button when loading is true', ...)The name tells the reader what the observable fact is. Present tense, active voice,
starting with a verb that describes what the unit does: returns, renders,
calls, throws, applies, dispatches, navigates, emits, updates.
For conditional behavior, embed the condition naturally:
it('renders the error message when the API call fails', ...)
it('disables the submit button while the form is submitting', ...)
it('returns the cached value on repeated calls within the debounce window', ...)File naming
Use ComponentName.test.tsx or useHookName.test.ts co-located next to the source
file. *.spec.* is equivalent but prefer .test.* for consistency.
Test Structure: AAA
Every test follows Arrange → Act → Assert with visual separation:
it('filters out inactive users', () => {
// Arrange
const users = [
createUser({ id: '1', active: true }),
createUser({ id: '2', active: false }),
]
// Act
const result = filterActiveUsers(users)
// Assert
expect(result).toHaveLength(1)
expect(result[0].id).toBe('1')
})Keep phases distinct. Don't interleave assertions with actions.
What to Test
Prioritize in this order:
- Business logic — the domain rules that are costly when wrong
- Edge cases —
null,undefined, empty arrays, zero, boundary values, max lengths - Error paths — thrown errors, rejected promises, invalid input
- User-visible behavior — what renders, what gets called, what the user sees change
Don't test:
- Third-party library internals (assume Vitest, React, React Testing Library work)
- Implementation details — private functions, internal component state, which helper was called inside another function
- Type definitions and interfaces alone (TypeScript already checks those)
Component Tests (React Testing Library)
Use @testing-library/react with Vitest. Always query by what the user sees.
Query priority (highest to lowest confidence)
getByRole → most semantic, preferred
getByLabelText → form elements
getByText → visible text
getByAltText → images
getByTestId → last resort; only when nothing else worksNever query by class name, CSS selector, or internal component name.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { SubmitButton } from './SubmitButton'
describe('SubmitButton', () => {
it('renders as disabled while loading', () => {
render(<SubmitButton loading={true} label="Save" />)
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled()
})
it('calls onClick when clicked and not loading', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(<SubmitButton loading={false} label="Save" onClick={handleClick} />)
await user.click(screen.getByRole('button', { name: /save/i }))
expect(handleClick).toHaveBeenCalledOnce()
})
})Prefer userEvent over fireEvent — it simulates real browser interactions
(pointer events, focus, etc.) rather than dispatching a single synthetic event.
Hook Tests
Use renderHook from @testing-library/react. Test through the hook's public
return values and callbacks — never inspect the hook's internal closure state.
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('initializes with the provided value', () => {
const { result } = renderHook(() => useCounter(5))
expect(result.current.count).toBe(5)
})
it('increments the count when increment is called', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
})Async Tests
Always await async assertions. Never leave a floating promise.
// ✅ Correct
it('loads and displays user data', async () => {
render(<UserProfile userId="42" />)
expect(await screen.findByText('Jane Doe')).toBeInTheDocument()
})
// ✅ For thrown rejections
it('throws when the user is not found', async () => {
await expect(fetchUser('nonexistent')).rejects.toThrow('User not found')
})
// ❌ Broken — test completes before the promise settles
it('loads user data', () => {
render(<UserProfile userId="42" />)
expect(screen.findByText('Jane Doe')).toBeInTheDocument() // no await
})Use findBy* (not waitFor + getBy*) when waiting for something to appear in the DOM.
Use waitForElementToBeRemoved when waiting for something to disappear.
Mocking Strategy
The rule: mock only what you can't control
- ✅ Mock: HTTP clients,
fetch, browser APIs (localStorage,matchMedia), third-party SDKs - ✅ Mock: Timers and dates when the test's behavior depends on elapsed time
- ❌ Don't mock: your own utility functions, React hooks, internal modules (test them together)
Module mocks
// Mock at the module level — hoisted automatically by Vitest
vi.mock('../api/users', () => ({
fetchUser: vi.fn(),
}))
// In the test, type the mock correctly
import { fetchUser } from '../api/users'
const mockedFetchUser = vi.mocked(fetchUser)
it('displays the user name after loading', async () => {
mockedFetchUser.mockResolvedValueOnce({ id: '1', name: 'Jane Doe' })
render(<UserProfile userId="1" />)
expect(await screen.findByText('Jane Doe')).toBeInTheDocument()
})Spy vs mock
vi.spyOn(obj, 'method')— observe or temporarily override a real method; restores automatically withrestoreMocks: truein configvi.fn()— standalone fake function with no original to restorevi.mock('./module')— replace an entire module for the whole file
Always reset between tests
Configure in vitest.config.ts rather than repeating in every file:
// vitest.config.ts
export default defineConfig({
test: {
clearMocks: true, // clears call history
restoreMocks: true, // restores spyOn originals
},
})TypeScript: Keep Types Honest
Never use as any in test code. If it won't type-check, the test data doesn't
match the real type — fix the data, not the type.
Type mocks correctly
import { vi } from 'vitest'
import type { UserService } from './UserService'
// Use vi.mocked for proper typing on mocked modules
vi.mock('./UserService')
const mockedService = vi.mocked(UserService)
// Use jest.Mocked<T> equivalent: vi.Mocked<T>
const mockService: vi.Mocked<UserService> = {
getUser: vi.fn(),
updateUser: vi.fn(),
}Factory functions for test data
Don't cast partial objects with as User. Build typed factories:
function createUser(overrides: Partial<User> = {}): User {
return {
id: 'test-id',
name: 'Test User',
email: 'test@example.com',
role: 'viewer',
createdAt: new Date('2024-01-01'),
...overrides,
}
}
// Usage
const admin = createUser({ role: 'admin' })
const anonymous = createUser({ id: undefined, name: 'Guest' })This pattern: keeps all tests type-safe, documents what a valid object looks like, gives each test fresh data (no shared mutation), and centralizes maintenance.
Fake Timers
Use fake timers when the unit's behavior depends on setTimeout, setInterval,
Date, or debounce/throttle logic.
describe('useDebounce', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers() // always restore — prevents leakage into other tests
})
it('returns the debounced value after the delay has elapsed', () => {
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 300),
{ initialProps: { value: 'initial' } },
)
rerender({ value: 'updated' })
expect(result.current).toBe('initial') // not yet
act(() => vi.advanceTimersByTime(300))
expect(result.current).toBe('updated') // now
})
})When fake timers and promises interact (e.g., a debounced fetch), use
vi.advanceTimersByTimeAsync(ms) instead — it flushes microtasks between
timer ticks and prevents hanging tests.
Parameterized Tests
Use test.each or test.for (Vitest-native) for repetitive cases over a data table
rather than copy-pasting tests:
describe('formatCurrency', () => {
test.each([
[0, 'USD', '$0.00'],
[1000, 'USD', '$1,000.00'],
[-50, 'USD', '-$50.00'],
[1234.5, 'EUR', '€1,234.50'],
])('formats %i %s as %s', (amount, currency, expected) => {
expect(formatCurrency(amount, currency)).toBe(expected)
})
})Snapshot Testing
Use snapshots sparingly. They catch regressions but don't communicate intent.
A snapshot-only test is nearly meaningless — reviewers approve --updateSnapshot
without reading the diff.
Good uses: serialized non-UI output (error messages, API payloads, complex strings). Bad uses: full component trees, anything with timestamps or IDs (use property matchers).
When you do use snapshots, keep them small and pair them with explicit assertions:
it('renders the error state', () => {
const { container } = render(<Alert type="error" message="Something went wrong" />)
// Explicit assertion first — communicates intent
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong')
// Snapshot catches unexpected structural regressions
expect(container.firstChild).toMatchInlineSnapshot(`...`)
})Anti-Patterns to Avoid
Don't test internals. If a test breaks when you rename a private variable or extract a helper function, it's testing implementation detail. Delete or rewrite it.
Don't assert in beforeEach. Assertions belong in test bodies so failures
are localized. beforeEach is for setup — cleanup, rendering, initializing mocks.
Don't share mutable state between tests. Variables set in beforeEach and
mutated across tests create order-dependent failures. Re-initialize per test or use
constants.
Don't mock everything. A test where the return value of every dependency is mocked isn't testing your code — it's testing that your code calls the mocks you told it to call. Keep real collaborators when you can.
Don't swallow async. An unawaited findBy* or waitFor will silently pass
while the real assertion never runs. Always await.
Don't use act unnecessarily. React Testing Library already wraps render,
fireEvent, userEvent, and findBy* in act. Wrapping again is noise.
An act(...) warning is a signal of a real problem — don't silence it with
an extra wrapper; fix the root cause.
Reference Files
For extended guidance, see:
references/component-patterns.md— patterns for context providers, portals, error boundariesreferences/mock-patterns.md—mswsetup, module mock factories,vi.mockhoisting caveatsreferences/async-patterns.md—waitFor,findBy, polling, WebSocket mocks
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

