
Vitest Dev
- 13 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
vitest-dev is a Claude Code skill for producing fast, deterministic, low-flake Vitest test suites for TypeScript and Next.js and tuning Vitest config for local and CI performance.
About
vitest-dev guides writing high-signal, low-flake, fast Vitest test suites for TypeScript and Next.js. It follows a defined procedure: map the unit under test, pick the lightest test, design a minimal matrix, implement, harden against flakiness, and optimize. A developer uses it when adding or improving tests and shaping Vitest config for local developer experience and CI throughput. It covers pool choice, isolation, parallelism, caching, and Next.js 16 integration defaults.
- Produces high-signal, low-flake, fast Vitest suites for TypeScript and Next.js 16
- Chooses the lightest test that gives confidence: pure unit, jsdom component, Node integration, or browser mode
- Tunes Vitest config (pool, isolate, workers, cache, sharding) for local DX and CI throughput
Vitest Dev by the numbers
- 13 all-time installs (skills.sh)
- Ranked #1,509 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
vitest-dev capabilities & compatibility
- Capabilities
- testing · unit testing · ci optimization · flake reduction
- Works with
- playwright
- Use cases
- testing · ci cd · debugging
- IDEs
- vscode · cursor ide
What vitest-dev says it does
A Claude Code skill for producing **high-signal, low-flake, fast** Vitest suites (TypeScript + Next.js 16)
Local development: `vitest` (watch mode by default when TTY is detected).
Prefer `threads` for “pure JS/TS” unit tests.
npx skills add https://github.com/bjornmelin/dev-skills --skill vitest-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Write fast, deterministic, low-flake Vitest suites for TypeScript and Next.js and tune config for local DX and CI throughput.
Who is it for?
Writing and optimizing Vitest suites and config for TypeScript/Next.js projects.
Skip if: Non-Vitest test runners or non-TypeScript stacks.
When should I use this skill?
Adding or improving tests, reducing flakiness, or tuning Vitest config for local DX and CI throughput.
What you get
A fast, deterministic Vitest suite with tuned config for local watch mode and scalable CI.
- Vitest test files
- Vitest config
- CI test scripts
By the numbers
- 8-step default operating procedure
- 4 Vitest pool options
Files
vitest-dev
A Claude Code skill for producing high-signal, low-flake, fast Vitest suites (TypeScript + Next.js 16) and for shaping Vitest configuration for optimal local DX and CI throughput.
Core outcomes
1. Correctness first: tests encode business behavior (not implementation details). 2. Deterministic: no network, no clocks, no global leakage, no order dependencies. 3. Fast:
- fast feedback locally (watch mode + smart filtering)
- scalable CI (parallel workers + sharding + cache + machine-readable reports)
4. Actionable failures: failures localize root cause quickly.
Default operating procedure
When asked to add or improve tests:
1. Map the unit under test
- Identify public API / observable behavior
- Identify boundaries: I/O, time, randomness, network, database, filesystem, env, global state
2. Choose the lightest test that gives confidence
- Pure unit test (no framework/runtime) → preferred
- Component test in
jsdom(React) when DOM behavior is essential - Integration test in Node when multiple modules must cooperate
- Browser Mode (real browser) only when DOM fidelity matters (layout/visuals, real events)
3. Design a minimal test matrix
- happy path(s)
- boundary conditions
- error paths
- key invariants (idempotency, caching semantics, auth gates, etc.)
4. Implement tests
- arrange/act/assert clarity
- isolate side effects and restore mocks
- prefer stable assertions (
toHaveTextContent, role-based queries, etc.)
5. Run locally and fix
- run smallest scope first (single file / name filtering)
6. Harden
- remove flakiness vectors (timers, concurrency, random, hidden network)
- ensure tests pass in “run mode” (CI-like)
7. Optimize
- reduce expensive setup per test file
- tune Vitest config: pool, isolate, workers, cache, deps optimization, sharding
8. Deliver
- include config + scripts changes needed for local + CI
- include README notes if non-obvious
Naming and structure conventions
- Place tests next to code for discoverability:
src/foo.ts→src/foo.test.tssrc/components/Button.tsx→src/components/Button.test.tsx- Use
__tests__for framework-driven routes when colocation is awkward (Next example uses this convention). - Prefer
describe('<unit>')with focusedit('does X when Y').
Vitest execution modes and what to target
- Local development:
vitest(watch mode by default when TTY is detected). - CI:
vitest run(forces a single run and is non-interactive).
From the Vitest CLI guide, Vitest defaults to watch mode when process.stdout.isTTY is true and falls back to run mode otherwise, while vitest run always runs once. (See: https://vitest.dev/guide/cli)
Configuration baseline
Recommended “default” config goals
- Use TypeScript path aliases (monorepos and Next apps frequently need this).
- Choose an environment per project:
nodefor backend/unit testsjsdom(orhappy-dom) for React component tests- Keep setup lightweight.
Pool choice (speed vs compatibility)
Vitest runs test files using a pool (forks, threads, vmThreads, vmForks). By default (Vitest v4 docs) it uses `forks`. threads can be faster but may break libraries that use native bindings; forks uses child_process and supports process APIs like process.chdir(). (See: https://vitest.dev/config/pool)
Rule of thumb:
- Prefer
threadsfor “pure JS/TS” unit tests. - Use
forksif you use: - native addons (e.g. Prisma, bcrypt, canvas)
process.*APIs that are not available in threads- Avoid VM pools unless you have measured wins and understand the tradeoffs.
Isolation (speed vs global leakage)
test.isolate defaults to true. Disabling can improve performance when tests don’t rely on side effects (often true for Node-only units). (See: https://vitest.dev/config/isolate)
Rule of thumb:
- Keep
isolate: truefor frontend/component tests (jsdom) and any suite that touches global state. - Consider
isolate: falsefor Node-only pure units after you have strong isolation discipline.
File-level and test-level parallelism
test.fileParallelismdefault istrue. Setting it tofalseforces single-worker execution by overridingmaxWorkersto 1. (See: https://vitest.dev/config/fileparallelism)test.maxWorkersdefaults to:- all available parallelism when watch is disabled
- half when watch is enabled
It also accepts a percentage string like "50%". (See: https://vitest.dev/config/maxworkers)
test.maxConcurrencycontrols how manytest.concurrenttests can run simultaneously, default5. (See: https://vitest.dev/config/maxconcurrency)
Cache (CI win)
Vitest caching is enabled by default and uses node_modules/.vite/vitest. (See: https://vitest.dev/config/cache)
For CI, persist this directory between runs (per branch key) for significant speedups.
Next.js 16 integration defaults
Use Next’s recommended baseline:
- Install (TypeScript):
vitest,@vitejs/plugin-react,jsdom,@testing-library/react,@testing-library/dom,vite-tsconfig-paths. - Configure
test.environment = 'jsdom'with the React + tsconfigPaths plugins.
(See: https://nextjs.org/docs/app/guides/testing/vitest)
Important limitation noted by Next.js: Vitest currently does not support async Server Components; for async components, use E2E tests instead. (See the same Next.js guide above.)
Mocking & test doubles discipline
Preferred hierarchy (from most realistic to most isolated)
1. Real pure functions (no mocking) 2. In-memory fakes (e.g., fake repo with a Map) 3. Contract-driven stubs (minimal, stable) 4. Spies (vi.spyOn) for verifying interactions 5. Module mocks (vi.mock) only when necessary
Mock reset policy
- Default: clean up per test file:
afterEach(() => vi.restoreAllMocks())- If you use global stubs (env/globals), clean them up in
afterEachtoo.
Timers
Use fake timers to avoid slow sleeps. Vitest’s docs show using vi.useFakeTimers() with vi.runAllTimers() / vi.advanceTimersByTime() to speed time-based code. (See: https://vitest.dev/guide/mocking/timers)
Advanced note: if you configure fakeTimers.toFake to include nextTick, it is not supported with --pool=forks because Node’s child_process uses process.nextTick internally and can hang; it is supported with --pool=threads. (See: https://vitest.dev/config/faketimers)
CI reporting and sharding
Reporters
- Use
junitto export JUnit XML (for most CI systems). (See: https://vitest.dev/guide/reporters) - In GitHub Actions, Vitest automatically adds
github-actionsreporter whenGITHUB_ACTIONS === 'true'if default reporters are used; if you override reporters, add it explicitly. (See: https://vitest.dev/guide/reporters)
Sharding (multi-machine parallel CI)
Use --shard with the blob reporter and merge at the end. Vitest recommends the blob reporter for sharded runs and provides --merge-reports. (See: https://vitest.dev/guide/reporters)
Using test projects for multi-environment suites
Use test.projects to run multiple configurations in one process (monorepos or mixed environments). Vitest notes the older “workspace” name is deprecated in favor of projects. (See: https://vitest.dev/guide/projects)
Patterns:
- project A:
environment: 'node',pool: 'threads',isolate: false - project B:
environment: 'jsdom',isolate: true
Deliverables this skill produces
When invoked, this skill can generate or update:
- Vitest config(s):
vitest.config.ts, multi-project configs, CI overrides - Test setup:
setupTests.ts, test utils, mocks - Tests: unit, integration, React component tests, type tests
- CI scripts: sharding + merging reports, coverage, flake detection
- Performance tuning recommendations with measurable steps
Output quality gates
Before finalizing, ensure:
- No test uses real timers (
setTimeoutwaits), real network, or real clock time without explicit control. - All mocks/stubs are restored.
- Tests pass with:
vitestvitest run- On CI, tests emit machine-readable artifacts (JUnit, JSON, blob merge) if requested.
- Coverage settings match team goals and don’t create “coverage theater”.
Where to look for authoritative details (official docs)
- Config reference: https://vitest.dev/config
- CLI: https://vitest.dev/guide/cli
- Improving performance: https://vitest.dev/guide/improving-performance
- Profiling performance: https://vitest.dev/guide/profiling-test-performance
- Parallelism: https://vitest.dev/guide/parallelism
- Mocking: https://vitest.dev/guide/mocking
- Reporters: https://vitest.dev/guide/reporters
- Coverage: https://vitest.dev/guide/coverage
- Next.js 16 + Vitest: https://nextjs.org/docs/app/guides/testing/vitest
CI optimization checklist (Vitest)
Mode
- [ ] CI uses
vitest run(single-run, non-interactive) - [ ] Watch mode is not relied upon
Parallelism
- [ ]
fileParallelismis enabled unless debugging - [ ]
maxWorkerstuned for CI resource limits (memory/cpu) - [ ] Optional: sharding used for multi-machine CI
Caching
- [ ] Persist
node_modules/.vite/vitestbetween CI runs (Vitest cache)
Reporting
- [ ] JUnit XML output configured when needed
- [ ] If using sharding:
- [ ]
blobreporter used per shard - [ ]
--merge-reportsrun in a final aggregation step
Coverage
- [ ] Coverage provider selected intentionally (v8 vs istanbul)
- [ ] Thresholds set intentionally (avoid “coverage theater”)
Flake detection (optional)
- [ ] High-risk tests are run multiple times in CI nightly or on demand
Test code review checklist
Design
- [ ] Tests assert behavior, not implementation details
- [ ] Each test name describes the scenario and expected outcome
- [ ] Avoids over-mocking; seams are mocked, core logic is real
Determinism
- [ ] No real network calls (unless explicitly an integration test)
- [ ] No real sleeps (
setTimeoutwaits); uses fake timers if time-based - [ ] No reliance on test order
Hygiene
- [ ] Mocks restored in
afterEach - [ ] Globals/env stubs are cleaned up
- [ ] Test fixtures are minimal and readable
Performance
- [ ] Avoids heavy global setup per test file
- [ ] Uses the lightest environment possible (
nodevsjsdom) - [ ] Does not introduce excessive
test.concurrentusage without reason
CI readiness
- [ ] Passes with
vitest run - [ ] Reports/artifacts are produced as required (JUnit/JSON/blob)
- [ ] Coverage config is intentional (no accidental slowdown)
QA test plan template (copy/paste)
1) Scope
- Feature / component:
- Public behavior / contract:
- Out of scope:
2) Risks
- What could break?
- What is most expensive to debug in production?
- What is performance sensitive?
3) Test matrix
Unit tests (fast, deterministic)
- [ ] Happy path
- [ ] Boundary conditions
- [ ] Error handling
- [ ] Invariants
Component tests (jsdom)
- [ ] Rendering
- [ ] Accessibility queries (roles/labels)
- [ ] User interactions
- [ ] Loading/error UI states
Integration tests (node)
- [ ] Module boundaries cooperate
- [ ] Database/network adapters are mocked/faked
E2E (if needed)
- [ ] Async Server Components (Next)
- [ ] Routing / auth / real browser behavior
4) Non-functional requirements
- [ ] Tests deterministic (no real network/clock)
- [ ] Tests run with
vitest run(CI-like) - [ ] Timeouts and concurrency are reasonable
- [ ] Coverage expectations documented (if enforced)
5) Exit criteria
- [ ] All tests pass locally and in CI
- [ ] Failures are actionable
- [ ] Flake check performed for risky areas (optional)
import React from 'react'
import { describe, expect, it, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// Mock next/navigation for client components
const push = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({ push }),
}))
import { ClientNavButton } from '../app/client-nav-button'
describe('<ClientNavButton />', () => {
it('navigates on click', async () => {
const user = userEvent.setup()
render(<ClientNavButton />)
await user.click(screen.getByRole('button', { name: /go to dashboard/i }))
expect(push).toHaveBeenCalledWith('/dashboard')
})
})
import React from 'react'
import { expect, test } from 'vitest'
import { render, screen } from '@testing-library/react'
import Page from '../app/page'
test('Page renders a heading', () => {
render(<Page />)
expect(screen.getByRole('heading', { level: 1, name: 'Home' })).toBeDefined()
})
'use client'
import React from 'react'
import { useRouter } from 'next/navigation'
export function ClientNavButton() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Go to dashboard
</button>
)
}
import Link from 'next/link'
export default function Page() {
return (
<div>
<h1>Home</h1>
<Link href="/about">About</Link>
</div>
)
}
import React from 'react'
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
describe('<Counter />', () => {
it('increments when the user clicks', async () => {
const user = userEvent.setup()
render(<Counter />)
expect(screen.getByLabelText('count')).toHaveTextContent('Count: 0')
await user.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByLabelText('count')).toHaveTextContent('Count: 1')
})
})
import React from 'react'
export function Counter() {
const [count, setCount] = React.useState(0)
return (
<div>
<p aria-label="count">Count: {count}</p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
Increment
</button>
</div>
)
}
import { describe, expect, it } from 'vitest'
import { add } from './add'
describe('add', () => {
it('adds positive numbers', () => {
expect(add(1, 2)).toBe(3)
})
it('adds negative numbers', () => {
expect(add(-1, -2)).toBe(-3)
})
it('handles zeros', () => {
expect(add(0, 5)).toBe(5)
expect(add(5, 0)).toBe(5)
})
})
export function add(a: number, b: number) {
return a + b
}
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
vitest-dev (Claude Code skill)
This folder is a self-contained skill package.
Install
1. Download the vitest-dev.zip artifact. 2. Unzip it into your Claude Code skills directory.
Common patterns:
- Project-local:
.claude/skills/vitest-dev/ - User-global:
~/.claude/skills/vitest-dev/
(Exact paths depend on your Claude Code setup; place the folder where your skills loader expects skill directories.)
Contents
skill.md– the skill definition and operating procedurechecklists/– QA checklists for test plan + review + CI readinessreferences/– distilled Vitest + Next.js guidance and quick referencestemplates/– ready-to-copy configs + setup files + utilitiesscripts/– CI/local helpers (sharding, merging reports, flake detection)examples/– example tests for TS, React, and Next.js
License
MIT (see LICENSE).
Official docs index (Vitest)
This skill is designed around the Vitest documentation structure (see the project’s /llms.txt map). The most relevant pages for day-to-day work are:
Daily workflow (start here)
- Getting started: https://vitest.dev/guide
- CLI: https://vitest.dev/guide/cli
- Filtering: https://vitest.dev/guide/filtering
- Environment: https://vitest.dev/guide/environment
- Mocking: https://vitest.dev/guide/mocking
- Coverage: https://vitest.dev/guide/coverage
- Reporters: https://vitest.dev/guide/reporters
- Projects: https://vitest.dev/guide/projects
Performance (local + CI)
- Profiling: https://vitest.dev/guide/profiling-test-performance
- Improving performance: https://vitest.dev/guide/improving-performance
Config knobs (most used for performance/stability)
- pool: https://vitest.dev/config/pool
- isolate: https://vitest.dev/config/isolate
- fileParallelism: https://vitest.dev/config/fileparallelism
- maxWorkers: https://vitest.dev/config/maxworkers
- maxConcurrency: https://vitest.dev/config/maxconcurrency
- cache: https://vitest.dev/config/cache
- deps: https://vitest.dev/config/deps
- setupFiles: https://vitest.dev/config/setupfiles
- coverage: https://vitest.dev/config/coverage
- typecheck: https://vitest.dev/config/typecheck
- fakeTimers: https://vitest.dev/config/faketimers
Next.js 16
- Next.js guide: https://nextjs.org/docs/app/guides/testing/vitest
Notes:
- Use the Vitest docs for authoritative behavior of Vitest flags and config.
- Use the Next.js guide for the canonical Next + Vitest integration path.
Performance playbook (Vitest)
This playbook focuses on turning a “works locally” suite into a fast, scalable suite on both developer machines and CI.
Step 1 — Measure first (profiling)
1. Run the slowest subset in CI-like mode:
vitest run
2. Identify hotspots:
- slow test files
- slow global setup
- expensive transforms and dependency processing
3. Only then change config. Avoid “cargo-cult” tuning.
See: https://vitest.dev/guide/profiling-test-performance
Step 2 — Choose the right pool
Vitest pools (https://vitest.dev/config/pool):
forks(default): runs tests inchild_processthreads: runs tests inworker_threads(often faster, but noprocess.chdir()and native modules can segfault)vmThreads/vmForks: uses Node’s VM context for speed, but has stability/memory tradeoffs (especially with ESM)
Recommendation:
- Start with
threadsfor pure TS/JS unit suites. - Use
forkswhen: - native bindings are involved
- you need process APIs in tests
Example:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
pool: process.env.CI ? 'forks' : 'threads',
},
})Step 3 — Isolation, safely
test.isolate (https://vitest.dev/config/isolate) defaults to true.
isolate: true: strongest protection against global leakageisolate: false: can improve performance only if you have strict discipline
Safe pattern:
- Use projects to keep
jsdomisolated but allow Node units to disable isolation.
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'unit-node',
test: {
environment: 'node',
isolate: false,
pool: 'threads',
},
},
{
name: 'ui-jsdom',
test: {
environment: 'jsdom',
isolate: true,
},
},
],
},
})Step 4 — Concurrency controls
File-level parallelism
test.fileParallelism (https://vitest.dev/config/fileparallelism) controls whether test files run in parallel.
- Turning it off forces
maxWorkers = 1.
Use it to:
- debug global leakage/order dependence
- reduce resource pressure in CI (rare)
Worker count
test.maxWorkers (https://vitest.dev/config/maxworkers):
- default uses all available parallelism in non-watch mode
- default uses half in watch mode
- accepts percent strings like
"50%"
Start with:
- local: default
- CI:
"50%"to reduce memory pressure (adjust after measuring)
test.concurrent
test.maxConcurrency (https://vitest.dev/config/maxconcurrency) defaults to 5 and limits the number of test.concurrent tests that can run at once.
Use test.concurrent sparingly; most suites get better throughput from file-level parallelism + multiple workers.
Step 5 — Cache (CI must-have)
Vitest cache config: https://vitest.dev/config/cache
cache.enableddefaulttruecache.dirdefaultnode_modules/.vite/vitest
In CI:
- persist
node_modules/.vite/vitestbetween runs - key the cache by:
- lockfile hash
- Node version
- OS
Step 6 — Sharding across CI machines
Vitest’s blob reporter stores results per machine so you can merge later (https://vitest.dev/guide/reporters).
Recommended flow:
1. On each CI node:
npx vitest run --shard=1/4 --reporter=blob --outputFile=reports/blob-1.json2. After all shards finish:
npx vitest --merge-reports=reports --reporter=default --reporter=jsonNotes:
--reporter=bloband--merge-reportsdon’t work in watch mode.- Prefer
vitest runin CI.
Step 7 — Reduce per-test overhead
Checklist:
- Avoid heavy
setupFileswork. They run before each test file (https://vitest.dev/config/setupfiles). - Move expensive global initialization into
globalSetuponly if truly required. - Prefer local fakes and dependency injection over module-level mocking in every file.
Step 8 — Coverage without killing performance
Coverage overview: https://vitest.dev/guide/coverage
- Vitest supports V8 coverage and Istanbul.
- V8 is typically faster and lower-memory, but can be slower when loading many modules and can’t easily limit coverage to specific modules.
- Istanbul is slower due to instrumentation overhead but can be limited to specific files.
Practical approach:
- CI: run coverage in a separate job (or nightly) if it slows the main pipeline too much.
- Use thresholds to prevent regressions, but avoid 100% “coverage theater”.
See thresholds: https://vitest.dev/config/coverage
Testing anti-patterns to avoid (Vitest)
1) Asserting implementation details
Bad:
- asserting internal state, private function calls, or exact DOM structure
- brittle snapshots of huge trees
Better:
- assert externally observable behavior (returned values, emitted events, DOM roles/text)
- use Testing Library queries (
getByRole,getByText) not CSS selectors
2) Sleeping (real timers) in tests
Bad:
await new Promise(r => setTimeout(r, 1000))
Better:
- use fake timers:
vi.useFakeTimers()vi.runAllTimers()/vi.advanceTimersByTime(...)
See: https://vitest.dev/guide/mocking/timers
3) Real network calls
Bad:
- tests depend on internet or real backend availability
Better:
- mock at the boundary
- stub fetch (
vi.stubGlobal('fetch', ...)) - or use MSW for request-level realism (recommended for component/integration tests)
4) Global leakage / order dependence
Symptoms:
- tests fail only when run together
- rerun fixes failures
Fixes:
- restore mocks every test (
vi.restoreAllMocks()) - reset modules if needed (
vi.resetModules()), but avoid doing it globally unless necessary - disable file parallelism temporarily to debug:
--no-file-parallelism(https://vitest.dev/config/fileparallelism)
5) Over-mocking modules
Bad:
- mocking core modules, “just because”
- huge
vi.mock(...)objects that replicate real code
Better:
- prefer dependency injection or thin adapter modules you can stub
- use
vi.spyOnfor narrow interaction assertions
6) Too much concurrency too early
Bad:
- sprinkling
test.concurrenteverywhere
Better:
- rely on worker parallelism first (
maxWorkers) - only use
test.concurrentwhen tests are I/O-bound and truly independent
test.concurrent is governed by maxConcurrency (default 5): https://vitest.dev/config/maxconcurrency
7) Running everything in jsdom
Bad:
- backend logic tested in DOM environment → slower, more globals
Better:
- multi-project setup:
- node project for most logic
- jsdom project only for UI component tests
See: https://vitest.dev/guide/projects
8) VM pools without measuring
Bad:
- using
vmThreadsbecause “it sounds faster”
Better:
- only adopt VM pools after profiling.
- understand the ESM/memory tradeoffs: https://vitest.dev/config/pool
Next.js 16 + Vitest setup (summary)
Canonical reference: https://nextjs.org/docs/app/guides/testing/vitest
Recommended dev dependencies (TypeScript)
From the Next.js guide:
vitest@vitejs/plugin-reactjsdom@testing-library/react@testing-library/domvite-tsconfig-paths
Minimal vitest.config.mts
The Next.js guide shows:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
},
})Limitations
The Next.js guide notes that Vitest currently does not support async Server Components; it recommends E2E tests for async components.
Practical additions (recommended)
1. setupTests.ts to install DOM matchers:
import '@testing-library/jest-dom/vitest'2. Add test.setupFiles in your Vitest config so matchers are always available.
3. Mock Next-specific modules as needed:
next/navigationnext/imagenext/router(pages router apps)
A pattern for next/navigation:
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams(),
}))Recommended test types in a Next app
- Pure unit tests: utilities, parsing, validation, domain rules (Node env).
- Component tests: client components (jsdom).
- E2E tests: async server components, routing, data fetching, authentication flows.
Mocking cheat sheet (Vitest)
Spies (preferred when possible)
import { vi } from 'vitest'
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
// ...
spy.mockRestore()Globals
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ ok: true }))))
// ...
vi.unstubAllGlobals()Environment variables
vi.stubEnv('API_URL', 'https://example.test')
// ...
vi.unstubAllEnvs()Module mocking (use sparingly)
Key principles:
- mock at the boundary
- keep mocks small and focused
- prefer returning real implementations with only the seam stubbed
vi.mock('../db', () => ({
getUser: vi.fn(),
}))Timers
Docs example: https://vitest.dev/guide/mocking/timers
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('runs after 2 hours', () => {
schedule(fn)
vi.runAllTimers()
expect(fn).toHaveBeenCalled()
})Advanced config: fakeTimers.toFake includes which globals to fake. If you include nextTick, it's not supported with --pool=forks (child_process can hang), but supported with --pool=threads. See: https://vitest.dev/config/faketimers
Vitest CLI cheat sheet
Primary reference: https://vitest.dev/guide/cli
Local development
# watch mode (TTY)
npx vitestCI
# run once
npx vitest runReporters
npx vitest run --reporter=junit
npx vitest run --reporter=json
npx vitest run --reporter=blob --outputFile=reports/blob.jsonSee reporters: https://vitest.dev/guide/reporters
Sharding
npx vitest run --shard=1/4 --reporter=blob --outputFile=reports/blob-1.json
npx vitest run --shard=2/4 --reporter=blob --outputFile=reports/blob-2.json
# merge:
npx vitest --merge-reports=reports --reporter=default --reporter=jsonDebugging global leakage
npx vitest run --no-file-parallelismConfig reference: https://vitest.dev/config/fileparallelism
Coverage + Typechecking reference
Coverage
Guide: https://vitest.dev/guide/coverage Config: https://vitest.dev/config/coverage
Key points:
- Coverage providers:
v8(default; fast, low memory; V8-only runtimes)istanbul(instrumented; slower; works everywhere)- Enable coverage:
- CLI:
vitest run --coverage - Config:
test.coverage.enabled = true - Include uncovered files by setting
coverage.includeto match your source globs. - Thresholds can be expressed as:
- positive: minimum percent required (e.g.
90) - negative: max uncovered items allowed (e.g.
-10)
See thresholds: https://vitest.dev/config/coverage
Typechecking (type tests)
Guide: https://vitest.dev/guide/testing-types Config: https://vitest.dev/config/typecheck
Key points:
- Type tests are files like
*.test-d.tsby default. - Vitest runs
tsc(orvue-tsc) under the hood and parses output. - Flags:
--typecheck/typecheck.enabled--typecheck.onlyto run only typecheck teststypecheck.ignoreSourceErrorscan be used to ignore non-test source errors (use carefully).
#!/usr/bin/env bash
set -euo pipefail
# CI runner for Vitest.
# - Forces single-run mode.
# - Emits JUnit + JSON to ./reports using Vitest's --outputFile.* flags.
# See: https://vitest.dev/config/outputfile
mkdir -p reports
npx vitest run --reporter=default --reporter=junit --reporter=json --outputFile.junit=reports/junit.xml --outputFile.json=reports/results.json
#!/usr/bin/env node
/**
* Simple flake detector: runs `vitest run` multiple times.
*
* Env vars:
* - FLAKE_RUNS (default 10)
* - FLAKE_FILTER (optional: passed as -t <pattern>)
*
* Usage:
* FLAKE_RUNS=20 node scripts/vitest-flake-check.mjs
*/
import { spawnSync } from 'node:child_process'
const runs = Number(process.env.FLAKE_RUNS || '10')
const filter = process.env.FLAKE_FILTER
if (!Number.isFinite(runs) || runs < 1) {
console.error('Invalid FLAKE_RUNS; must be a positive number.')
process.exit(2)
}
for (let i = 1; i <= runs; i++) {
console.log(`\n=== Flake run ${i}/${runs} ===\n`)
const args = ['vitest', 'run']
if (filter) args.push('-t', filter)
const res = spawnSync('npx', args, { stdio: 'inherit' })
if ((res.status ?? 1) !== 0) {
console.error(`\nFlake detected on run ${i}/${runs}.`)
process.exit(res.status ?? 1)
}
}
console.log(`\nNo failures in ${runs} runs.`)
#!/usr/bin/env bash
set -euo pipefail
# Local runner for Vitest.
# - Default: watch mode (TTY) using `vitest`
# - Optional UI: set VITEST_UI=1
if [[ "${VITEST_UI:-}" == "1" ]]; then
npx vitest --ui
else
npx vitest
fi
#!/usr/bin/env node
/**
* Merge Vitest blob reports.
*
* Expects blob JSON files in ./reports.
* See: https://vitest.dev/guide/reporters
*
* Usage:
* node scripts/vitest-merge-reports.mjs
*/
import { spawnSync } from 'node:child_process'
const args = [
'vitest',
'--merge-reports=reports',
'--reporter=default',
'--reporter=json',
'--reporter=junit',
'--outputFile.json=reports/merged-results.json',
'--outputFile.junit=reports/merged-junit.xml',
]
const res = spawnSync('npx', args, { stdio: 'inherit' })
process.exit(res.status ?? 1)
#!/usr/bin/env node
/**
* Vitest sharding runner.
*
* Uses env vars:
* - VITEST_SHARD_INDEX (1-based)
* - VITEST_SHARD_TOTAL
*
* Example:
* VITEST_SHARD_INDEX=1 VITEST_SHARD_TOTAL=4 node scripts/vitest-shard.mjs
*
* Writes a blob report so results can be merged.
* See:
* - blob reporter & merge: https://vitest.dev/guide/reporters
*/
import { spawnSync } from 'node:child_process'
import { mkdirSync } from 'node:fs'
const index = Number(process.env.VITEST_SHARD_INDEX || '')
const total = Number(process.env.VITEST_SHARD_TOTAL || '')
if (!Number.isFinite(index) || !Number.isFinite(total) || index < 1 || total < 1 || index > total) {
console.error('Invalid shard settings. Provide VITEST_SHARD_INDEX (1..N) and VITEST_SHARD_TOTAL (N).')
process.exit(2)
}
mkdirSync('reports', { recursive: true })
const shardArg = `--shard=${index}/${total}`
const outputFile = `reports/blob-${index}-of-${total}.json`
const args = [
'vitest',
'run',
shardArg,
'--reporter=blob',
`--outputFile=${outputFile}`,
]
// Inherit stdio for CI logs
const res = spawnSync('npx', args, { stdio: 'inherit' })
process.exit(res.status ?? 1)
{
"name": "vitest-dev",
"version": "0.1.0",
"entry": "skill.md",
"description": "World-class Vitest QA/test engineer for TypeScript + Next.js (local + CI performance focused)",
"files": [
"skill.md",
"README.md",
"LICENSE",
"checklists/",
"references/",
"templates/",
"scripts/",
"examples/"
]
}
import { afterEach, vi } from 'vitest'
// Node-only suites: keep global state clean by default.
afterEach(() => {
vi.restoreAllMocks()
})
import '@testing-library/jest-dom/vitest'
import { afterEach, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
// Keep tests hermetic by default.
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll } from 'vitest'
/**
* Optional MSW helper.
* Requires:
* npm i -D msw
*/
export const server = setupServer()
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
import { vi } from 'vitest'
/**
* Common Next.js module mocks.
* Use these only when needed; prefer testing pure logic without Next globals.
*/
export function mockNextNavigation() {
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams(),
}))
}
/**
* Next/Image often requires a mock in unit tests because it uses optimizations
* not available in jsdom.
*/
export function mockNextImage() {
vi.mock('next/image', () => ({
default: (props: any) => {
// eslint-disable-next-line jsx-a11y/alt-text
return <img {...props} />
},
}))
}
import React, { PropsWithChildren } from 'react'
import { render, RenderOptions } from '@testing-library/react'
function Providers({ children }: PropsWithChildren) {
// Add real providers here:
// - ThemeProvider
// - QueryClientProvider
// - Redux Provider
return <>{children}</>
}
export function renderWithProviders(
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) {
return render(ui, { wrapper: Providers, ...options })
}
import { defineConfig } from 'vitest/config'
/**
* Base config for TS/Node projects.
*
* Notes:
* - Prefer `threads` pool for pure TS/JS logic for speed.
* - Switch to `forks` if you depend on process APIs or native addons.
* - Consider `isolate: false` ONLY for Node-only units with strict discipline.
*/
export default defineConfig({
test: {
environment: 'node',
// Performance knobs
pool: process.env.VITEST_POOL as any || (process.env.CI ? 'forks' : 'threads'),
isolate: false,
fileParallelism: true,
// In CI, you might want to reduce workers to avoid OOM:
// maxWorkers: process.env.CI ? '50%' : undefined,
// Limits only tests marked with test.concurrent
maxConcurrency: 5,
// Cache (enabled by default). Pin the directory if you want stable CI caching.
cache: {
dir: 'node_modules/.vite/vitest',
},
// Keep setup files lightweight - they run before each test file.
// setupFiles: ['./test/setup-node.ts'],
// Optional: treat console noise as signal in CI
// silent: process.env.CI ? true : false,
},
})
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
/**
* Next.js + Vitest baseline aligned with Next's documentation.
*
* Reference:
* - https://nextjs.org/docs/app/guides/testing/vitest
*/
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
setupFiles: ['./test/setup-tests.ts'],
// Frontend suites benefit from isolation.
isolate: true,
// You can experiment with pool selection, but keep compatibility in mind.
pool: process.env.CI ? 'forks' : 'threads',
},
})
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
/**
* Multi-project example:
* - Fast Node unit tests (threads, isolate off)
* - UI tests in jsdom (isolate on)
*
* Reference:
* - https://vitest.dev/guide/projects
*/
export default defineConfig({
test: {
projects: [
{
name: 'unit-node',
test: {
environment: 'node',
pool: process.env.CI ? 'forks' : 'threads',
isolate: false,
include: ['src/**/*.test.ts'],
},
},
{
name: 'ui-jsdom',
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
isolate: true,
include: ['src/**/*.test.tsx'],
setupFiles: ['./test/setup-tests.ts'],
},
},
],
},
})
Related skills
FAQ
Which Vitest pool should I use?
Prefer threads for pure JS/TS unit tests, and use forks when you rely on native addons or process APIs not available in threads.
How does it run in CI vs local?
Local development uses vitest watch mode, while CI uses vitest run for a single non-interactive run.