Vitest Patterns You’ll Use Daily
You already use Vitest in ai-skills-sync, mysukari.com, and starlight-action. This covers patterns beyond the basics.
Config Patterns
Shared config with Vite (mysukari.com pattern)
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
coverage: {
provider: "v8",
include: ["src/**/*.ts", "src/**/*.tsx"],
},
},
});
Vitest reuses Vite’s plugin system. Your @vitejs/plugin-react and vite-tsconfig-paths work in both dev and test.
Minimal config (ai-skills-sync pattern)
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
passWithNoTests: true,
},
});
For CLI tools without DOM, you don’t even need an environment setting.
Testing Patterns
Mock external commands (CLI tools)
From ai-skills-sync: testing a CLI that calls git clone:
import { vi, describe, it, expect, beforeEach } from "vitest";
import { execa } from "execa";
vi.mock("execa", () => ({
execa: vi.fn(),
}));
const mockExeca = vi.mocked(execa);
beforeEach(() => {
mockExeca.mockReset();
});
it("clones the repo", async () => {
mockExeca.mockResolvedValueOnce({ stdout: "" } as any);
await fetchSkill("https://github.com/owner/repo");
expect(mockExeca).toHaveBeenCalledWith(
"git",
expect.arrayContaining(["clone"]),
expect.any(Object)
);
});
React component testing (mysukari.com pattern)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, it } from "vitest";
import { MyComponent } from "./MyComponent";
it("handles user interaction", async () => {
const user = userEvent.setup();
render(<MyComponent />);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText("Success")).toBeInTheDocument();
});
Snapshot testing
it("renders correctly", () => {
const { container } = render(<Card title="Test" />);
expect(container).toMatchSnapshot();
});
// Inline snapshots (auto-updated by vitest -u)
it("formats output", () => {
expect(formatDate(new Date("2026-01-01"))).toMatchInlineSnapshot(
`"January 1, 2026"`
);
});
Testing async operations with timeouts
import { vi, it, expect } from "vitest";
it("retries on failure", async () => {
vi.useFakeTimers();
const promise = fetchWithRetry("/api/data");
await vi.advanceTimersByTimeAsync(3000);
await expect(promise).resolves.toEqual({ data: "ok" });
vi.useRealTimers();
});
Coverage
V8 coverage (fast, native)
vitest run --coverage
// vitest.config.ts
test: {
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts", "src/types.ts"],
thresholds: {
statements: 80,
branches: 75,
},
},
}
Note: coverage include/exclude has a known bug in v4 with absolute paths (#9395). Use relative patterns.
Watch Mode Tricks
# Watch with UI dashboard
vitest --ui
# Filter by test name
vitest -t "should handle errors"
# Run only changed files
vitest --changed
# Run specific file
vitest src/utils.test.ts
Debugging Tests
# Run with Node inspector
vitest --inspect-brk --single-thread
# Dump transforms (see what Vite does to your code)
vitest --server.debug.dump
In VS Code, the Vitest extension gives you inline run/debug buttons.
Gotchas from Real Usage
-
vi.mock is hoisted - it runs before imports, regardless of where you write it in the file. This is by design but can be confusing.
-
jsdom vs happy-dom - jsdom is more complete, happy-dom is faster. For React testing with @testing-library, jsdom is safer.
-
Worker isolation - each test file runs in its own worker by default. Global state doesn’t leak between files but does leak between tests in the same file.
-
Coverage + vmForks - if you use
--pool=vmForks(for component tests in mysukari.com), coverage may need different config.
Next Steps
- Vitest Architecture - understand the internals
- Vitest Research - contribution opportunities