Vitest Testing
Overview
Vitest is a Vite-native unit testing framework that shares the same configuration and plugin ecosystem. Built for speed with native ESM support, hot module replacement for tests, and parallel execution.
When to use: Unit tests, component tests, integration tests, hook tests, in-source tests. Testing React components with React Testing Library. Testing TanStack Query/Router/Form patterns.
When NOT to use: End-to-end testing (use Playwright), visual regression testing (use Percy/Chromatic), load testing (use k6).
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| Test structure | describe('suite', () => {}) | Organize related tests |
| Single test | test('behavior', () => {}) or it() | Both are aliases |
| Parameterized tests | test.each([...])('name', (arg) => {}) | Run same test with different inputs |
| Concurrent tests | test.concurrent('name', async () => {}) | Run tests in parallel |
| Skip test | test.skip('name', () => {}) or skipIf(cond) | Conditionally skip tests |
| Only test | test.only('name', () => {}) | Run specific test for debugging |
| Assertions | expect(value).toBe(expected) | Compare primitive values |
| Deep equality | expect(obj).toEqual(expected) | Compare objects/arrays |
| Async assertions | await expect(promise).resolves.toBe(value) | Test promise resolution |
| Before each test | beforeEach(() => {}) | Setup before each test |
| After each test | afterEach(() => {}) | Cleanup after each test |
| Mock function | vi.fn() | Create spy function |
| Mock module | vi.mock('./module') | Replace entire module |
| Spy on method | vi.spyOn(obj, 'method') | Track calls to existing method |
| Clear mocks | vi.clearAllMocks() | Clear call history |
| Fake timers | vi.useFakeTimers() | Control setTimeout/setInterval |
| Render component | render(<Component />) | Mount React component for testing |
| Query by role | screen.getByRole('button') | Find elements by accessibility role |
| User interaction | await user.click(element) | Simulate user events |
| Wait for change | await waitFor(() => expect(...)) | Wait for async changes |
| Find async element | await screen.findByText('text') | Query + wait combined |
| Render hook | renderHook(() => useHook()) | Test custom hooks |
| Update hook props | rerender(newProps) | Re-render hook with new props |
| Snapshot | expect(value).toMatchSnapshot() | Compare against saved snapshot |
| Inline snapshot | expect(value).toMatchInlineSnapshot() | Snapshot stored in test file |
| CLI run once | vitest run | Single run, no watch |
| Run changed tests | vitest --changed | Tests affected by git changes |
| Filter by name | vitest -t "pattern" | Grep test names |
| Soft assertions | expect.soft(value).toBe(x) | Continue on failure, collect all |
| Poll assertions | await expect.poll(() => val).toBe(x) | Retry until passing |
| Test fixtures | const test = base.extend<F>({...}) | Reusable setup via test.extend |
| Hoisted mocks | vi.hoisted(() => ({ fn: vi.fn() })) | Variables for vi.mock factory |
| Shard tests | vitest --shard 1/3 | Split across CI workers |
| Tags | test('name', { tags: ['slow'] }, ...) | Filter with --tags-filter |
| Stub globals | vi.stubGlobal('fetch', vi.fn()) | Replace global objects cleanly |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using getBy for async content | Use findBy or waitFor for async |
| Testing implementation details | Test behavior and public API |
Using getByTestId as first choice | Prefer getByRole, getByLabelText, getByText |
Missing userEvent.setup() | Always call const user = userEvent.setup() first |
| Shared state between tests | Use beforeEach to reset or create fresh instances |
| Not cleaning up mocks | Use vi.clearAllMocks() in afterEach |
| Mocking too much | Only mock external dependencies and APIs |
| Not disabling retries in tests | Set retry: false for TanStack Query tests |
| Immediate assertions on async | Use await waitFor() or findBy queries |
| Creating QueryClient in render | Create once in wrapper or use useState |
| Testing library code | Trust the library, test your usage |
| Not awaiting user events | All user.* methods return promises |
Using act manually | userEvent and Testing Library handle this |
| Inline select without memoization | Extract to stable function for TanStack Query |
| Variables in vi.mock factory | Use vi.hoisted() to declare mock variables |
Single expect for multiple checks | Use expect.soft() to collect all failures |
setTimeout in tests for async waits | Use expect.poll() or vi.waitFor() |
Manual beforeEach for reusable setup | Use test.extend fixtures for composable setup |
Delegation
- Test discovery: For finding untested code paths, use
Exploreagent - Coverage analysis: For full coverage review, use
Taskagent - E2E testing: If
playwrightskill is available, delegate E2E testing to it - Code review: After writing tests, delegate to
code-revieweragent
References
- Test fundamentals: structure, assertions, lifecycle hooks
- Mocking: vi.fn, vi.mock, vi.spyOn, module mocking
- Component testing: React Testing Library, queries, user-event
- Hook testing: renderHook, async hooks, TanStack patterns
- Test setup: jest-dom matchers, cleanup, custom render, MSW, polyfills
- Configuration: vitest.config.ts, workspace, coverage, reporters
- Advanced patterns: snapshots, concurrent tests, in-source, type testing
- CLI and filtering: commands, watch mode, tags, sharding
- Fixtures and context: test.extend, scoping, auto fixtures, injection