
Vitest
- 125 installs
- 69 repo stars
- Updated August 4, 2026
- paulrberg/agent-skills
Helps with testing & qa tasks.
About
vitest is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- vitest
- Testing & QA
- AI-coding skill
Vitest by the numbers
- 125 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #935 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/paulrberg/agent-skills --skill vitestAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 125 |
|---|---|
| repo stars | ★ 69 |
| Last updated | August 4, 2026 |
| Repository | paulrberg/agent-skills ↗ |
What it does
Helps with testing & qa tasks.
Files
Your Role
You are an expert in writing tests with Vitest v4 for TypeScript React/Next.js projects. You help users write high-quality tests, debug failures, and maintain test suites efficiently.
Typical setup:
- Vitest v4 with jsdom environment
- Globals enabled (
describe,test,expect,vi) - Path aliases configured per project
Quick Start
Running Tests
# Run all unit tests
nlx vitest run
# Run tests matching pattern
nlx vitest run tokens
# Run specific test file
nlx vitest run src/utils/format.test.ts
# Run tests with matching name
nlx vitest run -t "adds token"
# Watch mode
nlx vitestWriting Your First Test
File naming: *.test.ts or *.test.tsx
Location: Colocate with source files
import { describe, test, expect } from "vitest";
import { myFunction } from "./my-function";
describe("myFunction", () => {
test("returns expected value", () => {
expect(myFunction(5)).toBe(10);
});
});Project-Specific Patterns
Test Organization
Use visual separators and descriptive blocks:
describe("TokenStore", () => {
/* ----------------------------------------------------------------
* Setup
* ------------------------------------------------------------- */
const validToken = { address: "0x123", symbol: "TEST" };
afterEach(() => {
// Reset state between tests
useTokensStore.getState().clearAll();
});
/* ----------------------------------------------------------------
* Adding tokens
* ------------------------------------------------------------- */
describe("addToken", () => {
test("adds valid token and returns true", () => {
const success = useTokensStore.getState().addToken(validToken);
expect(success).toBe(true);
});
});
});Cleanup Pattern
Always reset state in afterEach():
import { afterEach } from "vitest";
afterEach(() => {
// Reset mocks
vi.clearAllMocks();
// Reset environment
process.env.NODE_ENV = originalEnv;
// Reset stores
});Factory Mock Pattern
Prefer factory functions for complex mocks:
// __mocks__/localStorage.ts
import { vi } from "vitest";
export function createLocalStorageMock() {
const store = new Map<string, string>();
return {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
clear: vi.fn(() => {
store.clear();
})
};
}
// Usage in tests
import { createLocalStorageMock } from "./__mocks__/localStorage";
const mockStorage = createLocalStorageMock();
global.localStorage = mockStorage as Storage;Shared Setup File
Global mocks and configuration live in a setup file (e.g., tests/setup.ts):
import { vi } from "vitest";
// Mock logger for all tests
vi.mock("@/utils/logger", () => ({
createLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}))
}));Common Testing Scenarios
Testing Utilities
import { describe, test, expect, afterEach } from "vitest";
import { getEnvironment } from "./environment";
describe("getEnvironment", () => {
const originalEnv = process.env.NODE_ENV;
afterEach(() => {
process.env.NODE_ENV = originalEnv;
});
test("returns production when NODE_ENV is production", () => {
process.env.NODE_ENV = "production";
expect(getEnvironment()).toBe("production");
});
test("returns development by default", () => {
process.env.NODE_ENV = undefined;
expect(getEnvironment()).toBe("development");
});
});Async Testing
test("async function resolves correctly", async () => {
const result = await fetchData();
expect(result).toEqual({ data: "value" });
});
test("async function rejects with error", async () => {
await expect(failingFunction()).rejects.toThrow("Error message");
});Mocking
For function mocks (vi.fn, spyOn), module mocks (vi.mock, vi.doMock), and timer mocks (vi.useFakeTimers), see references/mocking.md.
Debugging Failed Tests
Reading Test Output
Focus on these signals:
- File and line number - Where the failure occurred
- Expected vs. received - What went wrong
- Stack trace - Ignore framework internals, focus on your code
Common Failures
For known failure modes and how to recover (timeouts, async assertions, mock not called, snapshot drift, etc.), see references/troubleshooting.md.
Debugging Tools
nlx vitest --reporter=verbose # Detailed output
nlx vitest --ui # Visual debugging interface
nlx vitest --coverage # See what's tested
nlx vitest --inspect # Node debugger
nlx vitest --run # Disable watch modeBest Practices
DO
- Colocate tests with source files (
feature.ts+feature.test.ts) - Use
describeblocks to group related tests - Add
afterEach()cleanup for state/mocks - Use visual separators for clarity (
/* --- */) - Test behavior, not implementation
- Use explicit type annotations for mocks
- Keep tests focused and independent
- Write tests before fixing bugs (reproduce the bug first)
DON'T
- Test implementation details (internal variables)
- Share state between tests
- Mock everything (only mock boundaries: network, storage, time)
- Forget to restore mocks/timers
- Use
anytypes in tests - Create brittle tests tied to DOM structure
- Add backward-compatibility hacks for test utilities
Advanced Topics
For deeper dives, see the ./references/ directory:
- `testing-patterns.md` - Complete pattern library (component tests, complex mocking, async patterns)
- `monorepo-testing.md` - Workspace-specific strategies (shared vs. app tests, path aliases, organization)
- `troubleshooting.md` - Debug guide (common errors, performance, coverage, CI/CD)
Coverage Analysis
To add coverage:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "json"],
exclude: ["**/*.test.ts", "**/__mocks__/**", "**/node_modules/**"]
}
}
});Run with: nlx vitest --coverage
Configuration Reference
Example config: vitest.config.ts
{
environment: "jsdom", // React/DOM APIs available
globals: true, // No imports needed for describe/test/expect
include: ["**/*.test.{js,ts,tsx}"],
exclude: ["**/node_modules/**", "**/e2e/**"],
setupFiles: ["./tests/setup.ts"],
alias: {
"@": "./src",
// Add your project's path aliases
},
}Next Steps
1. For component testing - See ./references/testing-patterns.md (React Testing Library setup) 2. For monorepo-specific strategies - See ./references/monorepo-testing.md 3. For debugging help - See ./references/troubleshooting.md
Start with simple unit tests, add component tests as needed.
policy:
allow_implicit_invocation: true
Mocking
When to read: when writing tests that mock functions, modules, or timers.
Mocking Functions
import { vi } from "vitest";
// Mock a function
const mockCallback = vi.fn((x: number) => x * 2);
mockCallback(5);
expect(mockCallback).toHaveBeenCalledWith(5);
expect(mockCallback).toHaveReturnedWith(10);
// Spy on object method
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
console.log("test");
expect(spy).toHaveBeenCalledWith("test");
spy.mockRestore();Mocking Modules
// At top level, before imports
vi.mock("./api-client", () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: "Test" }))
}));
import { fetchUser } from "./api-client";
test("uses mocked API", async () => {
const user = await fetchUser();
expect(user.name).toBe("Test");
});Timer Mocking
import { vi } from "vitest";
test("debounced function", () => {
vi.useFakeTimers();
const callback = vi.fn();
const debounced = debounce(callback, 1000);
debounced();
debounced();
debounced();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});Monorepo Testing Guide
Testing strategies for workspace monorepos with multiple apps and shared code.
Workspace Structure
monorepo/
├── apps/
│ ├── web/ # Main app
│ ├── admin/ # Admin app
│ └── docs/ # Documentation app
├── packages/
│ └── shared/ # Shared code across all apps
├── package.json # Root workspace config
├── vitest.config.ts # Shared test config
└── tests/
└── setup.ts # Global test setupKey principle: One Vitest config for all apps and shared code.
Test Organization
Recommended Structure
packages/shared/
├── tests/
│ └── setup.ts # Global setup (runs before all tests)
├── utils/
│ ├── cn.ts
│ ├── cn.test.ts # Colocate with source
│ ├── format.ts
│ └── format.test.ts
└── components/
├── Button.tsx
└── Button.test.tsx
apps/web/
├── lib/
│ └── stores/
│ ├── __mocks__/
│ │ └── localStorage.ts # App-specific mock
│ ├── user.ts
│ └── user.test.ts
└── app/
└── page.test.tsx # Page component test
apps/admin/
└── tests/ # E2E tests (excluded from unit tests)
└── dashboard.spec.tsPatterns:
- Shared code: Colocate tests with source
- App code: Colocate tests with source
- Mocks: Keep in
__mocks__/directory - Global setup:
tests/setup.tsorpackages/shared/tests/setup.ts - E2E tests: Separate
tests/directory (excluded from Vitest)
Where to Put Tests
In `packages/shared/` if:
- Utilities used by multiple apps
- Shared components
- Type definitions
- Constants
In `apps/<app-name>/` if:
- App-specific logic
- Store implementations
- Page components
- App-specific utilities
Example:
// packages/shared/utils/format-currency.ts
// packages/shared/utils/format-currency.test.ts
export function formatCurrency(amount: number) { ... }
// apps/web/lib/stores/user.ts
// apps/web/lib/stores/user.test.ts
export const useUserStore = create<UserStore>(...);Path Aliases
Configuration
vitest.config.ts:
export default defineConfig({
test: {
alias: {
"@shared": "./packages/shared",
"@web": "./apps/web",
"@admin": "./apps/admin"
}
}
});tsconfig.json:
{
"compilerOptions": {
"paths": {
"@shared/*": ["./packages/shared/*"],
"@web/*": ["./apps/web/*"],
"@admin/*": ["./apps/admin/*"]
}
}
}Usage in Tests
Prefer aliases over relative paths:
// Fragile relative paths
import { cn } from "../../../packages/shared/utils/cn";
import { useUserStore } from "../../lib/stores/user";
// Stable aliases
import { cn } from "@shared/utils/cn";
import { useUserStore } from "@web/lib/stores/user";Import from app-specific code:
// In apps/web/lib/stores/user.test.ts
import { useUserStore } from "@web/lib/stores/user";
import { createLocalStorageMock } from "@web/lib/stores/__mocks__/localStorage";Import shared code:
// Any test file
import { cn } from "@shared/utils/cn";
import { createLogger } from "@shared/utils/logger";Troubleshooting Aliases
Import not found:
1. Check vitest.config.ts alias matches import 2. Verify tsconfig.json paths match 3. Ensure no trailing slashes in config
// Wrong
alias: {
"@shared/": "./packages/shared/", // Trailing slash
}
// Correct
alias: {
"@shared": "./packages/shared",
}Running Tests by Scope
All Tests
nlx vitest run # Run all tests (shared + all apps)Shared Tests Only
nlx vitest run packages/shared/ # All shared tests
nlx vitest run packages/shared/utils/ # Specific shared directory
nlx vitest run packages/shared/utils/cn.test.ts # Specific fileApp-Specific Tests
nlx vitest run apps/web/ # All web app tests
nlx vitest run apps/admin/ # All admin app testsPattern Matching
nlx vitest run user # Any file matching "user"
nlx vitest run format # Any file matching "format"
nlx vitest run stores/ # Any file in stores/ directoryName-Based Filtering
nlx vitest run -t "adds user" # Tests with matching name
nlx vitest run -t "UserStore" # All tests in UserStore describe blocksCombined Filters
nlx vitest run apps/web/ -t "store" # Web tests with "store" in nameShared Setup Files
Global Setup File
tests/setup.ts runs before all tests:
import { vi } from "vitest";
// Mock logger for all tests
vi.mock("@shared/utils/logger", () => ({
createLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}))
}));
// Setup global test utilities
global.testUtils = {
// ...
};When to use:
- Mocks needed by all tests
- Global test utilities
- Environment variable defaults
- Browser API polyfills
When NOT to use:
- App-specific mocks
- Test data (use factories instead)
- Heavy initialization (slows all tests)
App-Specific Setup
Can be added per workspace:
// vitest.config.ts
export default defineConfig({
test: {
setupFiles: [
"./tests/setup.ts", // Global
"./apps/web/tests/setup.ts" // Web-specific
]
}
});Or use beforeAll in test files:
// apps/web/lib/stores/user.test.ts
import { beforeAll } from "vitest";
beforeAll(() => {
// Web-specific setup
});App-Specific vs Shared Mocks
Shared Mocks
Location: tests/ or inline in setup.ts
Use for:
- Utilities used by all apps
- Browser APIs (localStorage, fetch)
- Logger, analytics
Example:
// tests/setup.ts
vi.mock("@shared/utils/logger", () => ({
createLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}))
}));App-Specific Mocks
Location: apps/<app-name>/**/__mocks__/
Use for:
- App-specific stores
- App-specific API clients
- App-specific utilities
Example:
// apps/web/lib/stores/__mocks__/localStorage.ts
import { vi } from "vitest";
export function createLocalStorageMock() {
const store = new Map<string, string>();
return {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
clear: vi.fn(() => {
store.clear();
})
};
}Usage:
// apps/web/lib/stores/user.test.ts
import { createLocalStorageMock } from "./__mocks__/localStorage";
const mockStorage = createLocalStorageMock();
global.localStorage = mockStorage as Storage;Cross-App Dependencies
Testing Shared Code Used by Apps
Scenario: packages/shared/utils/cn.ts is used by all apps.
// packages/shared/utils/cn.test.ts
import { describe, test, expect } from "vitest";
import { cn } from "./cn";
describe("cn", () => {
test("merges class names", () => {
expect(cn("px-4", "text-sm")).toBe("px-4 text-sm");
});
test("handles conditionals", () => {
expect(cn("px-4", false && "text-sm")).toBe("px-4");
});
test("overrides conflicting classes", () => {
expect(cn("px-4", "px-6")).toBe("px-6");
});
});No need to test in each app - test once in shared.
Testing App Code That Uses Shared Code
Scenario: apps/web/lib/stores/user.ts uses @shared/utils/logger.
// apps/web/lib/stores/user.test.ts
import { describe, test, expect, vi } from "vitest";
import { useUserStore } from "./user";
import { createLogger } from "@shared/utils/logger";
// Logger is already mocked in tests/setup.ts
// No need to mock again
describe("UserStore", () => {
test("logs when user is added", () => {
const logger = createLogger("test");
useUserStore.getState().addUser({ id: "1", name: "Test" });
expect(logger.debug).toHaveBeenCalled();
});
});Workspace Dependencies
Testing Peer Dependencies
Apps may depend on each other (rare but possible):
// apps/admin/lib/use-web-feature.ts
import { useUserStore } from "@web/lib/stores/user";
// apps/admin/lib/use-web-feature.test.ts
import { useUserStore } from "@web/lib/stores/user";
// Test works because vitest.config.ts has alias configuredGenerally avoid cross-app imports - use shared code instead.
Parallel Execution
Vitest runs test files in parallel by default. Tests within a file run sequentially.
File-Level Parallelism
nlx vitest run # Runs all files in parallel
nlx vitest run --no-file-parallelism # Sequential file executionConcurrent Tests
import { test } from "vitest";
test.concurrent("parallel test 1", async () => {
// Runs in parallel with other concurrent tests
});
test.concurrent("parallel test 2", async () => {
// Runs in parallel with other concurrent tests
});
test("sequential test", () => {
// Runs after concurrent tests complete
});Use concurrent for:
- Independent tests
- No shared state
- API calls (with mocks)
Avoid concurrent for:
- Tests that modify global state
- Tests that use fake timers
- Tests that depend on execution order
Watch Mode Strategies
Full Watch
nlx vitest # Watch all filesVitest detects changes and re-runs affected tests.
Scoped Watch
nlx vitest packages/shared/ --watch # Watch only shared tests
nlx vitest apps/web/ --watch # Watch only web testsFilter in Watch Mode
While in watch mode, press keys:
a- Run all testsf- Run only failed testsp- Filter by filename patternt- Filter by test name patternq- Quit watch mode
Watch with UI
nlx vitest --ui # Watch + visual interfaceOpens browser UI for interactive testing.
CI/CD Considerations
Running All Tests
nlx vitest run # Disable watch mode (for CI)
nlx vitest run --reporter=verbose # Detailed output for CI logsCoverage in CI
nlx vitest run --coverage # Generate coverage report
nlx vitest run --coverage --reporter=json # JSON for toolingWorkspace-Specific CI
Run tests for changed workspaces only:
# Detect changed files
CHANGED=$(git diff --name-only HEAD~1 HEAD)
# Run tests for changed workspace
if echo "$CHANGED" | grep -q "^apps/web/"; then
nlx vitest run apps/web/
fi
if echo "$CHANGED" | grep -q "^packages/shared/"; then
nlx vitest run packages/shared/
# Also run app tests (shared code affects all apps)
nlx vitest run apps/
fiMonorepo Best Practices
DO
- Colocate tests with source files
- Use path aliases consistently
- Share setup files for common mocks
- Test shared code once, not per app
- Use pattern matching to run scoped tests
- Keep app-specific mocks in app directories
- Use factory functions for reusable mocks
DON'T
- Create separate test directories far from source
- Use relative imports when aliases exist
- Duplicate test setup across apps
- Test shared code in multiple apps
- Run full suite when working on single app
- Put app-specific mocks in shared/
- Hardcode mock data (use factories)
Performance Optimization
Faster Test Runs
1. Run only what changed:
nlx vitest run apps/web/ # Not entire suite2. Use pattern matching:
nlx vitest run user # Specific feature3. Parallelize when safe:
test.concurrent("independent test", async () => {
// Faster execution
});4. Avoid heavy setup:
// Slow: Runs for every test file
// tests/setup.ts
await seedDatabase();
// Fast: Only when needed
// specific-test.ts
beforeAll(async () => {
await seedDatabase();
});Optimizing Watch Mode
1. Use UI mode for debugging:
nlx vitest --ui # Better DX than terminal watch2. Filter aggressively:
nlx vitest -t "critical" # Only critical tests3. Disable coverage in dev:
nlx vitest # No coverage overheadDebugging Monorepo Tests
Import Resolution Issues
Symptom: Cannot find module '@shared/utils/cn'
Fix:
1. Check vitest.config.ts alias 2. Verify tsconfig.json paths 3. Restart IDE/dev server 4. Clear cache: rm -rf node_modules/.vite
Mock Not Working Across Apps
Symptom: Shared mock in setup.ts not applied to app tests
Fix:
1. Verify mock path matches import:
// setup.ts
vi.mock("@shared/utils/logger"); // Must use alias
// app test
import { logger } from "@shared/utils/logger"; // Same alias2. Check setup file is loaded:
// vitest.config.ts
setupFiles: ["./tests/setup.ts"],Test File Not Found
Symptom: No test files found
Fix:
1. Check file naming: *.test.ts or *.test.tsx 2. Verify not in exclude pattern:
// vitest.config.ts
exclude: ["**/node_modules/**", "**/e2e/**"], // Excludes e2e3. Check running from repo root
State Bleeding Between Tests
Symptom: Tests pass individually but fail when run together
Fix:
1. Add cleanup:
afterEach(() => {
vi.clearAllMocks();
// Reset stores, globals, etc.
});2. Isolate state:
// Shared state
const store = createStore();
// Fresh state per test
beforeEach(() => {
store = createStore();
});Next Steps
- For detailed testing patterns - See
TESTING_PATTERNS.md - For debugging strategies - See
TROUBLESHOOTING.md
Testing Patterns Reference
Complete pattern library for Vitest testing in TypeScript React/Next.js projects.
Unit Testing Patterns
Testing Pure Functions
import { describe, test, expect } from "vitest";
import { formatCurrency } from "./format-currency";
describe("formatCurrency", () => {
test("formats USD correctly", () => {
expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
});
test("handles zero", () => {
expect(formatCurrency(0, "USD")).toBe("$0.00");
});
test("handles negative values", () => {
expect(formatCurrency(-100, "USD")).toBe("-$100.00");
});
});Testing Functions with Side Effects
import { describe, test, expect, vi, afterEach } from "vitest";
import { logError } from "./error-logger";
describe("logError", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
afterEach(() => {
consoleSpy.mockClear();
});
test("logs error message", () => {
logError("Test error");
expect(consoleSpy).toHaveBeenCalledWith("Test error");
});
test("logs error object", () => {
const error = new Error("Something failed");
logError(error);
expect(consoleSpy).toHaveBeenCalledWith(error);
});
});Testing Utility Classes
import { describe, test, expect, beforeEach } from "vitest";
import { Cache } from "./cache";
describe("Cache", () => {
let cache: Cache<string>;
beforeEach(() => {
cache = new Cache<string>();
});
test("stores and retrieves values", () => {
cache.set("key", "value");
expect(cache.get("key")).toBe("value");
});
test("returns undefined for missing keys", () => {
expect(cache.get("missing")).toBeUndefined();
});
test("overwrites existing values", () => {
cache.set("key", "first");
cache.set("key", "second");
expect(cache.get("key")).toBe("second");
});
});Mocking Patterns
Basic Function Mocking
import { vi } from "vitest";
// Create mock function
const mockFn = vi.fn();
// With implementation
const mockAdd = vi.fn((a: number, b: number) => a + b);
// With return value
const mockGetUser = vi.fn().mockReturnValue({ id: 1, name: "Test" });
// With resolved promise
const mockFetch = vi.fn().mockResolvedValue({ data: "test" });
// With rejected promise
const mockFailingFetch = vi.fn().mockRejectedValue(new Error("Network error"));
// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
expect(mockFn).toHaveReturnedWith("value");Spying on Methods
import { vi } from "vitest";
test("spy on object method", () => {
const user = {
name: "John",
greet: () => `Hello, ${this.name}`
};
const greetSpy = vi.spyOn(user, "greet");
user.greet();
expect(greetSpy).toHaveBeenCalled();
greetSpy.mockRestore(); // Restore original implementation
});
test("spy with mock implementation", () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
console.log("test");
expect(spy).toHaveBeenCalledWith("test");
spy.mockRestore();
});Module Mocking
Automatic mock:
// At top level, before imports
vi.mock("./api-client");
import { fetchUser } from "./api-client";
test("uses automatic mock", () => {
// All exports are mocked automatically
expect(vi.isMockFunction(fetchUser)).toBe(true);
});Manual mock:
vi.mock("./api-client", () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: "Test" })),
deleteUser: vi.fn(() => Promise.resolve())
}));
import { fetchUser, deleteUser } from "./api-client";
test("uses manual mock", async () => {
const user = await fetchUser();
expect(user.name).toBe("Test");
});Partial mock:
import * as apiClient from "./api-client";
vi.spyOn(apiClient, "fetchUser").mockResolvedValue({
id: 1,
name: "Test"
});
// Other exports work normallyFactory mock with `vi.hoisted()`:
const { mockFetchUser } = vi.hoisted(() => ({
mockFetchUser: vi.fn()
}));
vi.mock("./api-client", () => ({
fetchUser: mockFetchUser
}));
test("can access mock before import", () => {
mockFetchUser.mockResolvedValue({ id: 1 });
// Now import and use
});Factory Mock Pattern
Create reusable mock factories:
// __mocks__/create-api-mock.ts
import { vi } from "vitest";
export function createApiMock() {
return {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn()
};
}
// In test file
import { createApiMock } from "./__mocks__/create-api-mock";
describe("API client", () => {
let api: ReturnType<typeof createApiMock>;
beforeEach(() => {
api = createApiMock();
});
test("makes GET request", async () => {
api.get.mockResolvedValue({ data: "test" });
const response = await api.get("/users");
expect(response.data).toBe("test");
});
});Complex factory with state:
// __mocks__/create-storage-mock.ts
import { vi } from "vitest";
export function createStorageMock() {
const store = new Map<string, string>();
return {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
clear: vi.fn(() => {
store.clear();
}),
get size() {
return store.size;
}
};
}Async Testing Patterns
Testing Promises
test("resolves with value", async () => {
const result = await Promise.resolve("success");
expect(result).toBe("success");
});
test("rejects with error", async () => {
await expect(Promise.reject(new Error("failed"))).rejects.toThrow("failed");
});
test("async function resolves", async () => {
async function fetchData() {
return { data: "value" };
}
const result = await fetchData();
expect(result).toEqual({ data: "value" });
});Testing Async Functions with Delays
import { vi } from "vitest";
test("waits for async operation", async () => {
const delayedFunction = async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
return "done";
};
const result = await delayedFunction();
expect(result).toBe("done");
});
test("uses fake timers for delays", async () => {
vi.useFakeTimers();
const promise = new Promise((resolve) => {
setTimeout(() => resolve("done"), 1000);
});
vi.advanceTimersByTime(1000);
const result = await promise;
expect(result).toBe("done");
vi.useRealTimers();
});Testing Callbacks
test("callback is called", (done) => {
function asyncOperation(callback: (result: string) => void) {
setTimeout(() => callback("success"), 10);
}
asyncOperation((result) => {
expect(result).toBe("success");
done(); // Signal test completion
});
});
// Or use promises (preferred)
test("callback is called with promise", async () => {
function asyncOperation(callback: (result: string) => void) {
setTimeout(() => callback("success"), 10);
}
const result = await new Promise((resolve) => {
asyncOperation(resolve);
});
expect(result).toBe("success");
});Testing Error Handling
test("handles async errors", async () => {
async function failingFunction() {
throw new Error("Something went wrong");
}
await expect(failingFunction()).rejects.toThrow("Something went wrong");
});
test("handles try-catch", async () => {
async function withErrorHandling() {
try {
throw new Error("Error");
} catch (error) {
return "handled";
}
}
const result = await withErrorHandling();
expect(result).toBe("handled");
});Timer and Date Mocking
Fake Timers
import { vi } from "vitest";
test("debounce function", () => {
vi.useFakeTimers();
const callback = vi.fn();
const debounced = debounce(callback, 1000);
debounced();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
test("throttle function", () => {
vi.useFakeTimers();
const callback = vi.fn();
const throttled = throttle(callback, 1000);
throttled();
throttled();
throttled();
expect(callback).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1000);
throttled();
expect(callback).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});System Time Mocking
import { vi } from "vitest";
test("mocks system time", () => {
const mockDate = new Date("2024-01-01T00:00:00.000Z");
vi.setSystemTime(mockDate);
expect(new Date().toISOString()).toBe("2024-01-01T00:00:00.000Z");
expect(Date.now()).toBe(mockDate.getTime());
vi.useRealTimers();
});
test("advances time", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-01"));
const start = Date.now();
vi.advanceTimersByTime(1000); // Advance 1 second
const end = Date.now();
expect(end - start).toBe(1000);
vi.useRealTimers();
});Testing Intervals
import { vi } from "vitest";
test("interval callback", () => {
vi.useFakeTimers();
const callback = vi.fn();
setInterval(callback, 1000);
vi.advanceTimersByTime(2500);
expect(callback).toHaveBeenCalledTimes(2);
vi.clearAllTimers();
vi.useRealTimers();
});Component Testing Setup
To add React Testing Library:
npm add -D @testing-library/react @testing-library/user-eventBasic Component Test
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { describe, test, expect, vi } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
test("renders children", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button")).toHaveTextContent("Click me");
});
test("calls onClick handler", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test("is disabled when disabled prop is true", () => {
render(<Button disabled>Click</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
});Testing with Props
import { render, screen } from "@testing-library/react";
test("renders with variant", () => {
render(<Button variant="primary">Click</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("bg-primary");
});
test("renders with custom className", () => {
render(<Button className="custom-class">Click</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("custom-class");
});Testing User Interactions
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
test("input value changes on type", async () => {
const user = userEvent.setup();
render(<input type="text" />);
const input = screen.getByRole("textbox");
await user.type(input, "Hello");
expect(input).toHaveValue("Hello");
});
test("form submission", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn((e) => e.preventDefault());
render(
<form onSubmit={handleSubmit}>
<input name="username" />
<button type="submit">Submit</button>
</form>,
);
await user.type(screen.getByRole("textbox"), "testuser");
await user.click(screen.getByRole("button"));
expect(handleSubmit).toHaveBeenCalledTimes(1);
});Testing Async Components
import { render, screen, waitFor } from "@testing-library/react";
test("shows loading state then data", async () => {
render(<UserProfile userId="1" />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
});
test("handles error state", async () => {
// Mock API to fail
vi.mock("./api", () => ({
fetchUser: vi.fn().mockRejectedValue(new Error("Failed")),
}));
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText("Error loading user")).toBeInTheDocument();
});
});Query Priorities
Use queries in this order:
1. getByRole - Accessible to everyone
screen.getByRole("button", { name: /submit/i });
screen.getByRole("textbox", { name: /username/i });2. getByLabelText - For form elements
screen.getByLabelText("Username");3. getByPlaceholderText - If no label
screen.getByPlaceholderText("Enter username");4. getByText - For non-interactive content
screen.getByText("Welcome");5. getByTestId - Last resort
screen.getByTestId("user-profile");Custom Render with Providers
// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { ReactElement } from "react";
function AllProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ThemeProvider>
);
}
export function renderWithProviders(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: AllProviders, ...options });
}
// In tests
import { renderWithProviders } from "./test-utils";
test("component with providers", () => {
renderWithProviders(<MyComponent />);
// ...
});Snapshot Testing
import { render } from "@testing-library/react";
test("matches snapshot", () => {
const { container } = render(<Button>Click me</Button>);
expect(container.firstChild).toMatchSnapshot();
});
// Update snapshots with: nlx vitest -uUse sparingly: Snapshots are brittle. Prefer targeted assertions.
Type Testing
Vitest can validate TypeScript types:
import { expectTypeOf } from "vitest";
test("type checks", () => {
expectTypeOf({ a: 1 }).toMatchTypeOf<{ a: number }>();
expectTypeOf([1, 2, 3]).toEqualTypeOf<number[]>();
expectTypeOf<string>().toBeString();
expectTypeOf<number>().toBeNumber();
});Custom Matchers
Extend expect with custom matchers:
import { expect } from "vitest";
expect.extend({
toBeWithinRange(received: number, min: number, max: number) {
const pass = received >= min && received <= max;
return {
pass,
message: () => `expected ${received} to be within range ${min} - ${max}`
};
}
});
// Usage
test("custom matcher", () => {
expect(5).toBeWithinRange(1, 10);
});Parameterized Tests
import { describe, test, expect } from "vitest";
describe.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 }
])("double($input)", ({ input, expected }) => {
test(`returns ${expected}`, () => {
expect(double(input)).toBe(expected);
});
});
// Or with test.each
test.each([
[1, 2],
[2, 4],
[3, 6]
])("double(%i) returns %i", (input, expected) => {
expect(double(input)).toBe(expected);
});Global Test Patterns
Setup and Teardown
import { beforeAll, afterAll, beforeEach, afterEach } from "vitest";
beforeAll(() => {
// Runs once before all tests in this file
});
afterAll(() => {
// Runs once after all tests in this file
});
beforeEach(() => {
// Runs before each test
});
afterEach(() => {
// Runs after each test
});Test Lifecycle
describe("nested describe blocks", () => {
beforeAll(() => console.log("1 - beforeAll"));
afterAll(() => console.log("1 - afterAll"));
beforeEach(() => console.log("1 - beforeEach"));
afterEach(() => console.log("1 - afterEach"));
test("test 1", () => console.log("1 - test"));
describe("nested", () => {
beforeAll(() => console.log("2 - beforeAll"));
afterAll(() => console.log("2 - afterAll"));
beforeEach(() => console.log("2 - beforeEach"));
afterEach(() => console.log("2 - afterEach"));
test("test 2", () => console.log("2 - test"));
});
});
// Output:
// 1 - beforeAll
// 2 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAllTest Filtering
// Run only this test
test.only("focused test", () => {
// ...
});
// Skip this test
test.skip("skipped test", () => {
// ...
});
// Skip conditionally
test.skipIf(process.env.CI)("runs only locally", () => {
// ...
});
// Run conditionally
test.runIf(process.platform === "darwin")("runs only on macOS", () => {
// ...
});
// Concurrent tests
test.concurrent("runs in parallel 1", async () => {
// ...
});
test.concurrent("runs in parallel 2", async () => {
// ...
});Next Steps
- For monorepo-specific testing - See
MONOREPO_TESTING.md - For debugging strategies - See
TROUBLESHOOTING.md
Troubleshooting Guide
Comprehensive debugging strategies for Vitest tests in TypeScript React/Next.js projects.
Common Errors
Import Errors
Cannot find module '@shared/...'
Cause: Path alias not configured or misconfigured.
Fix:
// vitest.config.ts
export default defineConfig({
test: {
alias: {
"@shared": "./packages/shared", // Must match tsconfig.json
"@web": "./apps/web"
}
}
});Verify:
# Check both configs match
cat vitest.config.ts | grep alias
cat tsconfig.json | grep pathsCannot find module '../../../utils/...'
Cause: Using relative imports instead of aliases.
Fix:
// Fragile
import { cn } from "../../../packages/shared/utils/cn";
// Stable
import { cn } from "@shared/utils/cn";Module not found during watch mode
Cause: Vitest cache stale after config changes.
Fix:
# Clear cache and restart
rm -rf node_modules/.vite
nlx vitestMock Errors
Mock is not a function
Cause: Mock not hoisted or module path mismatch.
Fix:
// Wrong path
vi.mock("./logger");
import { logger } from "@shared/utils/logger"; // Different path!
// Matching paths
vi.mock("@shared/utils/logger");
import { logger } from "@shared/utils/logger";Mock not applied to imports
Cause: Mock must be at top level, before imports.
Fix:
// Mock after import
import { fetchUser } from "./api";
vi.mock("./api");
// Mock before import
vi.mock("./api");
import { fetchUser } from "./api";Need to access mock before import
Use `vi.hoisted()`:
const { mockFetchUser } = vi.hoisted(() => ({
mockFetchUser: vi.fn()
}));
vi.mock("./api", () => ({
fetchUser: mockFetchUser
}));
// Now can configure before import
mockFetchUser.mockResolvedValue({ id: 1 });
import { fetchUser } from "./api";Global mock not applying to all tests
Cause: Mock in test file overrides global mock.
Fix:
// tests/setup.ts
vi.mock("@shared/utils/logger", () => ({
createLogger: vi.fn(() => ({ debug: vi.fn() }))
}));
// In test file - don't re-mock!
// This overrides global mock
vi.mock("@shared/utils/logger");
// Just import and use
import { createLogger } from "@shared/utils/logger";State Errors
Tests pass individually but fail together
Cause: State bleeding between tests.
Symptoms:
- Tests fail in different order
--no-file-parallelismmakes tests pass- First test always passes, second fails
Fix:
import { afterEach } from "vitest";
afterEach(() => {
// Reset mocks
vi.clearAllMocks();
vi.restoreAllMocks();
// Reset stores
useUserStore.getState().reset();
// Reset globals
delete global.localStorage;
// Reset environment
process.env.NODE_ENV = originalEnv;
});Debug:
# Run tests sequentially to confirm
nlx vitest run --no-file-parallelism
# Run specific test file alone
nlx vitest run path/to/problem.test.tsStore state persists between tests
For stores:
import { afterEach, beforeEach } from "vitest";
let store: Store;
beforeEach(() => {
// Create fresh instance
store = createStore();
});
afterEach(() => {
// Cleanup
store.destroy();
});Global variables polluted
Cause: Tests modify global scope without cleanup.
Fix:
describe("localStorage tests", () => {
const originalLocalStorage = global.localStorage;
afterEach(() => {
global.localStorage = originalLocalStorage;
});
test("mocks localStorage", () => {
global.localStorage = createLocalStorageMock();
// Test...
});
});Async Errors
Test times out
Symptoms:
Error: Test timeout of 5000ms exceededCauses:
1. Unresolved promise 2. Missing await 3. Infinite loop 4. Slow operation
Fix 1: Increase timeout
test("slow operation", async () => {
await slowFunction();
}, 10000); // 10 second timeoutFix 2: Find unresolved promise
// Missing await
test("async test", () => {
fetchData(); // Promise not awaited!
expect(data).toBeDefined();
});
// Await promise
test("async test", async () => {
const data = await fetchData();
expect(data).toBeDefined();
});Fix 3: Check for infinite loops
// Infinite retry
async function fetchWithRetry() {
while (true) { // Never exits!
try {
return await fetch("/api");
} catch {
// Retry forever
}
}
}
// Limited retries
async function fetchWithRetry(maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch("/api");
} catch {
if (i === maxRetries - 1) throw;
}
}
}Promise rejection not handled
Symptom:
UnhandledPromiseRejectionWarningFix:
// Rejection not caught
test("failing api", async () => {
await fetchData(); // Throws, but not caught
});
// Expect rejection
test("failing api", async () => {
await expect(fetchData()).rejects.toThrow("Error");
});
// Try-catch
test("failing api", async () => {
try {
await fetchData();
expect.fail("Should have thrown");
} catch (error) {
expect(error.message).toBe("Error");
}
});Fake timers not advancing
Cause: Timer not advanced or wrong timer API used.
Fix:
import { vi } from "vitest";
test("debounce with timers", () => {
vi.useFakeTimers();
const callback = vi.fn();
const debounced = debounce(callback, 1000);
debounced();
// Timer not advanced
expect(callback).toHaveBeenCalled(); // Fails!
// Advance timer
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled(); // Passes
vi.useRealTimers();
});Remember to restore:
afterEach(() => {
vi.useRealTimers();
});Type Errors
Type error in test file
Cause: Mock types don't match real types.
Fix:
// Wrong type
const mockFetch = vi.fn(); // Returns unknown
// Typed mock
const mockFetch = vi.fn<[string], Promise<Response>>();
// Or type assertion
const mockFetch = vi.fn() as MockedFunction<typeof fetch>;Cannot find type definitions
Cause: Missing @types package or wrong tsconfig.
Fix:
# Install missing types
npm add -D @types/node
# Check tsconfig includes test files
cat tsconfig.json | grep "include"// tsconfig.json
{
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.test.ts", // Include test files
"**/*.test.tsx"
]
}Configuration Errors
Tests not found
Symptoms:
No test files foundCauses:
1. Wrong file naming 2. Excluded by config 3. Running from wrong directory
Fix 1: Check file naming
# Must match pattern
*.test.ts
*.test.tsx
# Wrong
*_test.ts
*.spec.ts # Unless configuredFix 2: Check exclude pattern
// vitest.config.ts
export default defineConfig({
test: {
include: ["**/*.test.{js,ts,tsx}"],
exclude: [
"**/node_modules/**",
"**/e2e/**" // Excludes e2e tests
]
}
});Fix 3: Run from repo root
# Wrong directory
cd apps/web
nlx vitest run
# From repo root
cd /path/to/monorepo
nlx vitest run apps/web/Environment not jsdom
Symptom: document is not defined
Fix:
// vitest.config.ts
export default defineConfig({
test: {
environment: "jsdom" // Required for React tests
}
});Or per-file:
// @vitest-environment jsdom
import { render } from "@testing-library/react";Globals not available
Symptom: describe is not defined
Fix:
// vitest.config.ts
export default defineConfig({
test: {
globals: true // Makes describe/test/expect global
}
});Or import explicitly:
import { describe, test, expect } from "vitest";Debugging Strategies
Reading Test Output
Focus on these signals:
FAIL apps/web/lib/stores/user.test.ts > UserStore > addUser > validates user format
AssertionError: expected false to be true
- Expected: true
+ Received: false
45| test("validates user format", () => {
46| const invalid = { id: "invalid" };
47| const result = useUserStore.getState().addUser(invalid);
> 48| expect(result).toBe(true);
49| });Key information:
1. File path: apps/web/lib/stores/user.test.ts 2. Test path: UserStore > addUser > validates user format 3. Error type: AssertionError 4. Expected vs Received: true vs false 5. Line number: 48
Ignore:
- Vitest internal stack traces
- Framework setup calls
- Node module paths
Verbose Output
nlx vitest run --reporter=verboseShows:
- All passing tests (not just failures)
- Test execution time
- Detailed stack traces
- Console output from tests
UI Mode
nlx vitest --uiFeatures:
- Visual test tree
- Click to run individual tests
- Real-time updates
- Detailed failure info
- Console output viewer
Best for:
- Debugging single test
- Exploring test structure
- Interactive development
Node Debugger
nlx vitest --inspectThen open Chrome DevTools and set breakpoints.
Or use `debugger` statement:
test("debug this", () => {
const value = calculateValue();
debugger; // Pause here
expect(value).toBe(10);
});Console Debugging
test("debug with console", () => {
const store = useUserStore.getState();
console.log("Before:", store.users);
store.addUser(newUser);
console.log("After:", store.users);
expect(store.users).toHaveLength(1);
});View console output:
nlx vitest run --reporter=verbose # Shows console.logIsolating Failures
Run only failing test:
test.only("this one test", () => {
// Runs alone
});Skip passing tests:
test.skip("passing test", () => {
// Skipped
});Run failed tests only:
nlx vitest # Start watch mode
# Press 'f' to run only failed testsBisecting Test Runs
Find which test causes failure:
# Run first half
nlx vitest run apps/web/lib/stores/
# Run second half
nlx vitest run apps/web/app/
# Narrow down to specific file
nlx vitest run apps/web/lib/stores/user.test.tsPerformance Optimization
Slow Test Suite
Symptoms:
- Tests take > 30s total
- Watch mode feels sluggish
- CI times out
Diagnose:
nlx vitest run --reporter=verbose # Shows test timesFixes:
1. Reduce setup overhead:
// Slow: Heavy setup runs for every test
beforeEach(async () => {
await seedDatabase(); // Slow!
});
// Fast: Setup once per file
beforeAll(async () => {
await seedDatabase();
});
// Fast: Or use mocks instead
beforeEach(() => {
mockDatabase.seed(); // Fast!
});2. Parallelize independent tests:
test.concurrent("independent 1", async () => {
// Runs in parallel
});
test.concurrent("independent 2", async () => {
// Runs in parallel
});3. Avoid real timers:
// Slow: Real delay
test("debounce", async () => {
debounced();
await sleep(1000); // Slow!
expect(callback).toHaveBeenCalled();
});
// Fast: Fake timers
test("debounce", () => {
vi.useFakeTimers();
debounced();
vi.advanceTimersByTime(1000); // Instant!
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
});4. Mock expensive operations:
// Slow: Real API calls
test("fetches data", async () => {
const data = await fetch("/api/users"); // Slow!
});
// Fast: Mock API
vi.mock("./api", () => ({
fetchUsers: vi.fn(() => Promise.resolve(mockUsers))
}));
test("fetches data", async () => {
const data = await fetchUsers(); // Fast!
});5. Use scoped test runs:
# Run all tests while developing
nlx vitest run
# Run only related tests
nlx vitest run user
nlx vitest run apps/web/lib/stores/Memory Issues
Symptoms:
- Process killed
- Out of memory errors
- Tests hang
Fixes:
1. Increase Node memory:
NODE_OPTIONS="--max-old-space-size=4096" nlx vitest run2. Clean up after tests:
afterEach(() => {
vi.clearAllMocks();
// Clean up large objects
store.clear();
});3. Avoid memory leaks:
// Event listeners not removed
test("adds listener", () => {
window.addEventListener("resize", handler);
// Leaks!
});
// Clean up listeners
test("adds listener", () => {
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
});Coverage Issues
Low Coverage
Check what's missing:
nlx vitest run --coverageOpens HTML report showing uncovered lines.
Common uncovered code:
- Error handlers (test error paths!)
- Edge cases (test boundary conditions!)
- Default branches (test all branches!)
Coverage Not Accurate
Cause: Excluded files or wrong provider.
Fix:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: "v8", // More accurate than istanbul
include: ["apps/**/*.ts", "packages/**/*.ts"],
exclude: ["**/*.test.ts", "**/__mocks__/**", "**/node_modules/**", "**/*.d.ts"]
}
}
});Coverage Thresholds Failing
Symptom:
Coverage threshold for lines (80%) not met: 75%Options:
1. Add more tests (preferred)
2. Adjust thresholds (if realistic)
// vitest.config.ts
coverage: {
lines: 75,
functions: 75,
branches: 75,
statements: 75,
}3. Exclude files (use sparingly)
coverage: {
exclude: [
"**/*.config.ts",
"**/types/**",
],
}CI/CD Integration
Tests Fail in CI but Pass Locally
Common causes:
1. Different Node versions:
# .github/workflows/test.yml
- uses: actions/setup-node@v3
with:
node-version: "20" # Match local version2. Different timezones:
// Fix: Mock dates
vi.setSystemTime(new Date("2024-01-01T00:00:00.000Z"));3. Parallelism differences:
# CI: Disable parallelism if causing issues
nlx vitest run --no-file-parallelism4. Missing environment variables:
# .github/workflows/test.yml
env:
NODE_ENV: test
CI: true5. Race conditions:
// Fix: Add proper awaits
await waitFor(() => {
expect(element).toBeInTheDocument();
});CI Performance
Speed up CI tests:
1. Cache dependencies:
# .github/workflows/test.yml
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}2. Run tests in parallel:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: nlx vitest run --shard=${{ matrix.shard }}/43. Only test changed code:
- run: |
if [[ $(git diff --name-only HEAD~1 | grep "^apps/web/") ]]; then
nlx vitest run apps/web/
fi4. Skip coverage on PR:
# Run coverage only on main
if [[ $GITHUB_REF == "refs/heads/main" ]]; then
nlx vitest run --coverage
else
nlx vitest run
fiDebugging CI Failures
1. Enable verbose output:
- run: nlx vitest run --reporter=verbose2. Upload test results:
- uses: actions/upload-artifact@v3
if: failure()
with:
name: test-results
path: test-results/3. Run CI locally:
# Install act: https://github.com/nektos/act
act -j test4. SSH into CI:
# Add to workflow for debugging
- uses: mxschmitt/action-tmate@v3
if: failure()Common Failures from SKILL.md
State bleeding between tests:
// Problem: Previous test left state
test("first test", () => {
store.addItem("test");
});
test("second test", () => {
expect(store.items).toHaveLength(0); // Fails! Still has "test"
});
// Solution: Add cleanup
afterEach(() => {
store.clear();
});Mock not working:
// Problem: Mock path doesn't match import
vi.mock("./utils/logger");
import { logger } from "@/utils/logger"; // Different path!
// Solution: Match exact import path
vi.mock("@/utils/logger");Async timeout:
// Problem: Default 5s timeout too short
test("slow operation", async () => {
await verySlowOperation(); // Times out
});
// Solution: Increase timeout
test("slow operation", async () => {
await verySlowOperation();
}, 10000); // 10 second timeoutNext Steps
- For testing patterns - See
TESTING_PATTERNS.md - For monorepo strategies - See
MONOREPO_TESTING.md