vitest-testing
SKILL.md
Vitest Testing Patterns
Setup
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/testing/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.test.*", "src/testing/**"],
},
},
});
// src/testing/setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(() => {
cleanup();
});
File Organization
Colocate test files with the code they test:
components/
└── button/
├── button.tsx
├── button.test.tsx
└── index.ts
hooks/
└── use-debounce.ts
└── use-debounce.test.ts
testing/
├── setup.ts # global test setup
├── utils.tsx # custom render, providers
├── handlers.ts # MSW handlers
└── factories.ts # test data factories
Writing Tests
Structure
describe("calculateTotal", () => {
it("sums item prices", () => {
const items = [{ price: 10 }, { price: 20 }];
expect(calculateTotal(items)).toBe(30);
});
it("returns 0 for empty array", () => {
expect(calculateTotal([])).toBe(0);
});
it("applies discount when provided", () => {
const items = [{ price: 100 }];
expect(calculateTotal(items, { discount: 0.1 })).toBe(90);
});
});
- Test behavior, not implementation.
- One assertion per test when possible — makes failures easy to diagnose.
- Use descriptive names:
it("returns 0 for empty array"), notit("works").
Component Testing
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
it("submits with email and password", async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
it("shows validation error for invalid email", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.type(screen.getByLabelText("Email"), "invalid");
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByText("Invalid email address")).toBeInTheDocument();
});
});
Query Priority
Use queries in this order (most accessible to least):
getByRole— buttons, links, headings, inputs by rolegetByLabelText— form fieldsgetByPlaceholderText— when no label existsgetByText— non-interactive elementsgetByTestId— last resort
If getByRole can't find the element, the component likely has an accessibility gap.
Custom Render
Wrap with providers used throughout the app:
// testing/utils.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, type RenderOptions } from "@testing-library/react";
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};
}
export function renderWithProviders(ui: React.ReactElement, options?: Omit<RenderOptions, "wrapper">) {
return render(ui, { wrapper: createWrapper(), ...options });
}
Mocking
Functions
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ id: "1", name: "Test" });
mockFn.mockImplementation((x: number) => x * 2);
expect(mockFn).toHaveBeenCalledWith("arg");
expect(mockFn).toHaveBeenCalledTimes(1);
Spying
const spy = vi.spyOn(userService, "getUser");
spy.mockResolvedValue(mockUser);
// ... run code ...
expect(spy).toHaveBeenCalledWith("user-123");
spy.mockRestore();
Modules
vi.mock("@/lib/api-client", () => ({
api: {
get: vi.fn(),
post: vi.fn(),
},
}));
Timers
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("debounces input", async () => {
render(<SearchInput />);
await userEvent.type(screen.getByRole("textbox"), "query");
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledWith("query");
});
Cleanup
Always restore mocks to prevent leaking between tests:
afterEach(() => {
vi.restoreAllMocks();
});
MSW Integration
Mock API responses at the network level:
npm install -D msw
// testing/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
]);
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: "3", ...body }, { status: 201 });
}),
];
// testing/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// testing/setup.ts
import { server } from "./server";
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Override handlers per test:
it("shows error state on API failure", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json({ message: "Server error" }, { status: 500 });
})
);
renderWithProviders(<UserList />);
expect(await screen.findByText("Failed to load users")).toBeInTheDocument();
});
Testing Hooks
import { renderHook, act } from "@testing-library/react";
describe("useCounter", () => {
it("increments count", () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Test Data Factories
Create consistent test data:
// testing/factories.ts
let idCounter = 0;
export function createUser(overrides?: Partial<User>): User {
idCounter++;
return {
id: `user-${idCounter}`,
name: `User ${idCounter}`,
email: `user${idCounter}@test.com`,
role: "member",
...overrides,
};
}
Anti-Patterns
- Don't test implementation — test what the user sees and does, not internal state.
- Don't snapshot everything — snapshots are brittle. Use them only for small, stable outputs.
- Don't mock what you don't own — mock the boundary (MSW for HTTP,
vi.mockfor modules), not library internals. - Don't use
getByTestIdfirst — reach for accessible queries first. - Don't forget
await—userEventandfindBy*queries are async.
Weekly Installs
2
Repository
grahamcrackers/skillsFirst Seen
14 days ago
Security Audits
Installed on
cline2
github-copilot2
codex2
kimi-cli2
gemini-cli2
cursor2