CtrlK
BlogDocsLog inGet started
Tessl Logo

vitest-testing-skill

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-skill
What are skills?

76

Does it follow best practices?

Validation for skill structure

SKILL.md
Review
Evals

Vitest Testing Skill

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.

Prerequisites

  • Node.js >= 20.x
  • Vite-based project (or any TypeScript project)
  • npm or pnpm

Scaffold Command

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

Project Structure

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)

Key Conventions

  • Co-locate tests with source files using .test.ts or .test.tsx suffix.
  • Use describe for grouping, it or test for individual test cases.
  • vi is the global mock utility (equivalent to jest in Jest).
  • Use vi.fn() for mock functions, vi.mock() for module mocking, vi.spyOn() for partial mocking.
  • For component tests, use Testing Library with jsdom environment. Avoid testing implementation details.
  • Use @testing-library/user-event for realistic user interactions (not fireEvent).
  • Vitest runs in watch mode by default during development. Use vitest run for single-run (CI).

Essential Patterns

Vitest Configuration (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,
      },
    },
  },
});

If Using vite.config.ts (Shared Config)

/// <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 (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();
});

Unit Test — Utility Function (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...");
  });
});

Mocking with vi.fn() and vi.mock()

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");
  });
});

vi.spyOn() — Partial Mocking

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");
  });
});

Component Test — React (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");
  });
});

Custom Render with Providers (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 });
}

Hook Testing

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);
  });
});

Snapshot Testing

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)"`);
  });
});

Timer and Date Mocking

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");
  });
});

Workspace Config (vitest.workspace.ts) — Multi-Project

import { 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"],
    },
  },
]);

Mocking HTTP APIs (MSW)

// 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();
  });
});

Common Commands

# 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

Integration Notes

  • React: Use @testing-library/react for component tests. Set environment: "jsdom" in Vitest config. Use the custom render helper for provider wrapping.
  • Vue: Use @testing-library/vue or @vue/test-utils. Same jsdom environment.
  • Vite: Vitest shares Vite's config (plugins, aliases, etc.), making it the natural testing choice for Vite projects.
  • Jest Migration: Vitest is API-compatible with Jest. Replace jest.fn() with vi.fn(), jest.mock() with vi.mock(), etc. Most tests work with minimal changes.
  • CI/CD: Pair with github-actions-ci skill. Run npx vitest run --coverage in CI. Use --reporter=junit for CI report integration.
  • Playwright: Use Vitest for unit/component tests, Playwright for E2E tests. They complement each other.

Test Database Setup

# 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.

Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.