Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
Install with Tessl CLI
npx tessl i github:achreftlili/deep-dev-skills --skill vitest-testing-skill76
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
npm install -D vitest
# For React component testing
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
# For Vue component testing
npm install -D @testing-library/vue @testing-library/jest-dom jsdom
# For coverage
npm install -D @vitest/coverage-v8src/
components/
Button/
Button.tsx
Button.test.tsx # Component test — co-located
hooks/
useAuth.ts
useAuth.test.ts
utils/
format.ts
format.test.ts
lib/
api-client.ts
__mocks__/
api-client.ts # Manual mock
test/
setup.ts # Global test setup (Testing Library matchers, etc.)
helpers/
render.tsx # Custom render with providers
factories.ts # Test data factories
vitest.config.ts # Vitest configuration (or vite.config.ts)
vitest.workspace.ts # Multi-project workspace (optional).test.ts or .test.tsx suffix.describe for grouping, it or test for individual test cases.vi is the global mock utility (equivalent to jest in Jest).vi.fn() for mock functions, vi.mock() for module mocking, vi.spyOn() for partial mocking.jsdom environment. Avoid testing implementation details.@testing-library/user-event for realistic user interactions (not fireEvent).vitest run for single-run (CI).vitest.config.ts)import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
include: ["src/**/*.test.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.d.ts",
"src/**/index.ts",
"src/main.tsx",
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
});/// <reference types="vitest/config" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
css: true,
},
});test/setup.ts)import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
// Automatically clean up after each test
afterEach(() => {
cleanup();
});src/utils/format.test.ts)import { describe, it, expect } from "vitest";
import { formatCurrency, formatDate, truncate } from "./format";
describe("formatCurrency", () => {
it("should format USD amounts with 2 decimal places", () => {
expect(formatCurrency(1234.5)).toBe("$1,234.50");
});
it("should handle zero", () => {
expect(formatCurrency(0)).toBe("$0.00");
});
it("should handle negative amounts", () => {
expect(formatCurrency(-50)).toBe("-$50.00");
});
});
describe("truncate", () => {
it("should return the full string if shorter than max length", () => {
expect(truncate("hello", 10)).toBe("hello");
});
it("should truncate and add ellipsis when exceeding max length", () => {
expect(truncate("hello world", 5)).toBe("hello...");
});
});import { describe, it, expect, vi, beforeEach } from "vitest";
import { fetchUsers } from "./user-service";
// Mock the entire module
vi.mock("@/lib/api-client", () => ({
apiClient: {
get: vi.fn(),
},
}));
import { apiClient } from "@/lib/api-client";
describe("fetchUsers", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should return users from the API", async () => {
const mockUsers = [
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
];
vi.mocked(apiClient.get).mockResolvedValue({ data: mockUsers });
const result = await fetchUsers();
expect(result).toEqual(mockUsers);
expect(apiClient.get).toHaveBeenCalledWith("/users");
});
it("should throw when API returns an error", async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error("Network error"));
await expect(fetchUsers()).rejects.toThrow("Network error");
});
});import { describe, it, expect, vi } from "vitest";
import * as dateUtils from "./date-utils";
describe("getGreeting", () => {
it("should return 'Good morning' before noon", () => {
vi.spyOn(dateUtils, "getCurrentHour").mockReturnValue(9);
expect(dateUtils.getGreeting()).toBe("Good morning");
vi.restoreAllMocks();
});
it("should return 'Good evening' after 6pm", () => {
vi.spyOn(dateUtils, "getCurrentHour").mockReturnValue(20);
expect(dateUtils.getGreeting()).toBe("Good evening");
});
});src/components/Button/Button.test.tsx)import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
describe("Button", () => {
it("should render with the correct text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("should call onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("should be disabled when loading", () => {
render(<Button loading>Submit</Button>);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
expect(button).toHaveTextContent("Loading...");
});
it("should apply the correct variant class", () => {
render(<Button variant="danger">Delete</Button>);
expect(screen.getByRole("button")).toHaveClass("btn-danger");
});
});test/helpers/render.tsx)import type { ReactElement } from "react";
import { render, type RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AuthProvider } from "@/features/auth/context/AuthContext";
function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
);
}
export function renderWithProviders(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(ui, { wrapper: AllProviders, ...options });
}import { describe, it, expect, vi } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
it("should initialize with the given value", () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it("should increment the counter", () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it("should default to 0", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
});import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
import { UserCard } from "./UserCard";
describe("UserCard", () => {
it("should match snapshot", () => {
const { container } = render(
<UserCard name="Alice" email="alice@example.com" role="admin" />
);
expect(container.firstChild).toMatchSnapshot();
});
it("should match inline snapshot", () => {
const result = formatUserDisplay({ name: "Alice", role: "admin" });
expect(result).toMatchInlineSnapshot(`"Alice (admin)"`);
});
});import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
describe("delayed operation", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should execute callback after delay", () => {
const callback = vi.fn();
scheduleTask(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
it("should use mocked date", () => {
vi.setSystemTime(new Date("2024-06-15T12:00:00Z"));
expect(new Date().toISOString()).toBe("2024-06-15T12:00:00.000Z");
});
});vitest.workspace.ts) — Multi-Projectimport { defineWorkspace } from "vitest/config";
export default defineWorkspace([
{
test: {
name: "unit",
include: ["src/**/*.test.ts"],
environment: "node",
},
},
{
test: {
name: "components",
include: ["src/**/*.test.tsx"],
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
},
},
]);// tests/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([
{ id: "1", name: "Alice", email: "alice@test.com" },
]);
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: "2", ...body }, { status: 201 });
}),
];// tests/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);// test/setup.ts — add MSW lifecycle hooks alongside Testing Library cleanup
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => {
cleanup();
server.resetHandlers();
});
afterAll(() => server.close());// src/features/users/users.test.ts
import { describe, it, expect } from "vitest";
import { http, HttpResponse } from "msw";
import { server } from "../../test/mocks/server";
import { fetchUsers } from "./users-service";
describe("Users API integration", () => {
it("should fetch users from the API", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0]).toMatchObject({ id: "1", name: "Alice" });
});
it("should handle API errors", async () => {
server.use(
http.get("/api/users", () => {
return HttpResponse.json({ message: "Server error" }, { status: 500 });
})
);
await expect(fetchUsers()).rejects.toThrow();
});
});# Run tests in watch mode (default)
npx vitest
# Run tests once (CI mode)
npx vitest run
# Run specific test file
npx vitest src/utils/format.test.ts
# Run tests matching a pattern
npx vitest --reporter=verbose --testNamePattern="should format"
# Run with coverage
npx vitest run --coverage
# Run with UI
npx vitest --ui
# Update snapshots
npx vitest run --update
# Run only changed tests
npx vitest --changed
# Type-check tests
npx vitest typecheck
# Run specific workspace project
npx vitest --project=unit
npx vitest --project=components@testing-library/react for component tests. Set environment: "jsdom" in Vitest config. Use the custom render helper for provider wrapping.@testing-library/vue or @vue/test-utils. Same jsdom environment.jest.fn() with vi.fn(), jest.mock() with vi.mock(), etc. Most tests work with minimal changes.github-actions-ci skill. Run npx vitest run --coverage in CI. Use --reporter=junit for CI report integration.# docker-compose.test.yml
services:
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data # RAM-backed for speedUse DATABASE_URL=postgresql://test:test@localhost:5433/testdb in your test environment. Start with docker compose -f docker-compose.test.yml up -d before running integration tests.
181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.