testing/vitest-testing-skill/SKILL.md
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.
npx skillsauth add achreftlili/deep-dev-skills vitest-testing-skillInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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-v8
src/
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="[email protected]" 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: "[email protected]" },
]);
}),
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 speed
Use 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.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.
tools
--- name: react-native-project-starter description: > Scaffold a production-ready React Native 0.76+ app with Expo SDK 52+, Expo Router for file-based navigation, Zustand for state management, TypeScript, EAS Build, platform-specific code, and AsyncStorage. category: mobile agent-type: coding compatibility: Node.js >= 20.x, npm >= 10.x, Expo CLI, EAS CLI, iOS: Xcode 16+, Android: Android Studio with SDK 35+ --- # React Native Project Starter > Scaffold a production-ready React Native 0.7