typescript-testing

TypeScript/JavaScript testing patterns — Vitest (preferred) and Jest, with test organization, mocking, snapshot testing, coverage, fixtures, and integration with Playwright. Use when writing or reviewing TypeScript/JavaScript unit and integration tests.

TypeScript Testing

Testing patterns for TypeScript/JavaScript, covering Vitest (preferred) and Jest, with integration for E2E tests via Playwright (separate skill).

When to Activate

  • Writing unit or integration tests in TS/JS
  • Setting up Vitest or Jest for a new project
  • Mocking modules, time, fetch, filesystem
  • Writing snapshot tests, coverage reports
  • Deciding between Vitest and Jest for a new project

Vitest vs Jest — quick take

AspectVitestJest
ESM supportNative (Vite-based)Via Babel or experimental
TypeScriptNative via ViteVia ts-jest or Babel
SpeedFaster (HMR)Slower
Configvitest.config.ts (shareable with Vite)jest.config.ts
EcosystemNew, growing fastMature, many plugins
APIJest-compatible

Default: Vitest for new projects. Jest if already present in the project.

Setup — Vitest

bun add -d vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node", // or "jsdom" for components
    coverage: {
      provider: "v8",
      reporter: ["text", "lcov"],
      lines: 80,
      functions: 80,
      branches: 75,
      statements: 80,
    },
    setupFiles: ["./tests/setup.ts"],
  },
});
// package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui"
  }
}

Test structure

src/
├── users/
│   ├── users.service.ts
│   └── users.service.test.ts    # colocate (preferred)
└── utils/
    └── date.ts
tests/
├── setup.ts                      # global mocks
├── fixtures/                     # shared data
└── integration/                  # tests hitting real DB, HTTP

Pattern: unit tests colocated (next to the code); integration tests in a separate directory.

Test basics

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { UsersService } from "./users.service";

describe("UsersService", () => {
  let service: UsersService;

  beforeEach(() => {
    service = new UsersService(mockRepo());
  });

  describe("create", () => {
    it("should hash the password before storing", async () => {
      const user = await service.create({ email: "[email protected]", password: "secret123456" });
      expect(user.passwordHash).not.toBe("secret123456");
      expect(user.passwordHash).toMatch(/^\$2[aby]\$/);  // bcrypt
    });

    it("should throw on duplicate email", async () => {
      await service.create({ email: "[email protected]", password: "secret123456" });
      await expect(
        service.create({ email: "[email protected]", password: "different12345" })
      ).rejects.toThrow(/already exists/);
    });
  });
});

Rules:

  • One describe per class/module, nested for methods
  • it describes behavior (not implementation): "should hash the password" not "calls bcrypt"
  • beforeEach creates a fresh instance; prevents leaks between tests
  • expect(...).rejects.toThrow() for async errors

Mocking with vi

import { vi } from "vitest";

// Whole-module mock
vi.mock("./email-service", () => ({
  sendEmail: vi.fn().mockResolvedValue({ id: "fake" }),
}));

// Partial mock
vi.mock("./config", async (importOriginal) => {
  const actual = await importOriginal<typeof import("./config")>();
  return { ...actual, API_KEY: "test-key" };
});

// Spy on existing method
const spy = vi.spyOn(repo, "findByEmail").mockResolvedValue(null);

// Fake timers
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-01"));
// ... test code
vi.useRealTimers();

Rules:

  • Reset mocks in afterEach with vi.restoreAllMocks()
  • Never mock the code under test — mock its dependencies
  • For HTTP: use MSW (msw/node) instead of mocking fetch

Parametrized tests

it.each([
  { input: "[email protected]", expected: true },
  { input: "invalid", expected: false },
  { input: "", expected: false },
  { input: "a@b", expected: false },
])("isValidEmail($input) → $expected", ({ input, expected }) => {
  expect(isValidEmail(input)).toBe(expected);
});

Snapshot tests

Use sparingly — only for stable, reviewable output:

it("renders the user card correctly", () => {
  const { container } = render(<UserCard user={mockUser} />);
  expect(container).toMatchSnapshot();
});

Avoid inline snapshots for large objects — they grow out of control.

Integration tests

For tests that hit a real DB:

// tests/integration/users.test.ts
import { beforeAll, afterAll, beforeEach, describe, it, expect } from "vitest";
import { Pool } from "pg";
import { migrate } from "../../src/db/migrate";

let pool: Pool;

beforeAll(async () => {
  pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
  await migrate(pool);
});

afterAll(async () => {
  await pool.end();
});

beforeEach(async () => {
  await pool.query("TRUNCATE users CASCADE");
});

describe("Users repository (integration)", () => {
  it("persists and retrieves a user", async () => {
    const repo = new UsersRepository(pool);
    const created = await repo.insert({ email: "[email protected]" });
    const loaded = await repo.findById(created.id);
    expect(loaded?.email).toBe("[email protected]");
  });
});

Pattern: truncate between tests instead of rollback — simpler and works with any driver.

Coverage

Set explicit thresholds (see vitest.config.ts above). Exclude:

coverage: {
  exclude: [
    "**/*.d.ts",
    "**/*.config.*",
    "**/types/**",
    "**/__mocks__/**",
    "src/index.ts",       // usually just bootstrap
  ],
}

Jest differences

If the project uses Jest instead of Vitest, swap vi.* for jest.*. The API is compatible in ~95% of cases. Main differences:

  • vi.mock vs jest.mock (near-identical semantics)
  • Config in jest.config.ts instead of vitest.config.ts
  • ts-jest or Babel transformer required for TS

Playwright (integration / E2E)

For in-browser E2E tests, use the e2e-testing skill separately — Playwright is treated as a distinct layer. For unit tests of code that interacts with Playwright, mock page.* with vi.fn() and test only the custom logic.

Related skills

  • test-driven-development — RED-GREEN-REFACTOR cycle + git checkpoints + coverage ≥80%
  • e2e-testing — Playwright E2E (complementary)
  • bun-runtime — alternative to vitest run via bun test
  • verification-before-completion — pre-commit verification