Unit Testing Skill (Vitest)
Write fast, isolated unit tests with Vitest for TypeScript logic — utilities, Zod schemas, hooks,
and Server Actions (with dependencies mocked). Unit tests do NOT render components in a browser
(use the storybook-testing skill for that).
This file holds the rules. Code lives in the references:
- references/examples.md — seven worked examples: pure utility, Zod schema,
Server Action with mocked DB, transform function, parameterized (
it.each), async error handling, hooks.
- references/patterns.md — AAA, test isolation, mock boundaries, the
vi.mock vs vi.fn vs vi.spyOn decision table, coverage targets, and anti-patterns.
- references/mocking.md — the full mocking catalogue (factories, hoisting,
partial/dynamic mocks, async, DB/auth/time/env recipes). Read before writing non-trivial mocks.
Global Test Utilities
This project enables global test utilities — do not import them:
// ❌ Not needed
import { describe, test, expect, vi, beforeEach } from "vitest";
// ✅ Available globally: describe, test (= it), expect, beforeEach/afterEach/beforeAll/afterAll, vi
Configured via globals: true in vitest.config.ts and "types": ["vitest/globals"] in tsconfig.json.
Workflow
- Analyze the code — inputs, outputs, side effects, dependencies, and every branch.
- Identify cases — happy path, edge cases, error paths, boundary values.
- Write tests — AAA pattern (Arrange, Act, Assert); mock external dependencies at module boundaries.
- Run —
npm run test:unit; verify all pass and check coverage.
File Naming
Tests live next to the source file, named <source-filename>.test.ts (.test.tsx if it imports
React/JSX):
src/utils/format-currency.ts → src/utils/format-currency.test.ts
src/features/budgets/actions/create-budget.ts → .../create-budget.test.ts
Core Rules
- AAA structure — Arrange, Act, Assert; group with nested
describe (module → function → case).
- Test behavior, not implementation — assert on return values and observable effects, never on
private internals; don't test private functions or abuse snapshots.
- Mock only at module boundaries — external dependencies (DB, auth, network, time, UUID). Never
mock the internal utility you're testing.
- Pick the right mock tool (full decision table in patterns.md):
vi.mock("module", factory) — replace an entire module (DB, auth).
vi.fn() — a standalone mock (callback / injected dependency).
vi.spyOn(obj, "method") — observe or replace one method, restorable via .mockRestore().
- Reset between tests —
beforeEach(() => vi.clearAllMocks()); afterEach(() => vi.restoreAllMocks()).
- Type your mocks —
vi.mocked(fn).mockResolvedValue(...).
- Parameterize repetitive cases with
test.each([...]) instead of copy-pasting tests.
- Async —
await the call; use await expect(p).rejects.toThrow(...) / .resolves.toMatchObject(...).
- Pure functions need no mocking — the easiest and highest-value tests.
Mocking
The three core tools (vi.fn, vi.mock, vi.spyOn) are summarized above. Two more worth knowing:
vi.hoisted(() => ({ ... })) when a vi.mock factory needs shared mock references, and
vi.mock(import("./m"), async (orig) => ({ ...(await orig()), one: vi.fn() })) for partial mocks that
keep the real exports.
The full catalogue — async resolution, hoisting edge cases, third-party libraries, and database/auth
recipes — is in references/mocking.md.
Setup (vitest.config.ts essentials)
test: {
globals: true, // no test-utility imports needed
environment: "node", // "jsdom" for hooks/DOM
include: ["src/**/*.test.{ts,tsx}"],
coverage: {
provider: "v8",
exclude: ["src/**/*.test.{ts,tsx}", "src/**/*.stories.{ts,tsx}", "src/**/index.ts", "src/types/**"],
},
}
Running Tests
npm run test:unit # all unit tests
npm run test:unit -- --watch # watch mode
npm run test:unit -- src/utils/format-currency.test.ts # one file
npm run test:unit -- --grep "formatCurrency" # by pattern
npm run test:unit -- --coverage # coverage report
Questions to Ask
- What are the function's inputs and expected outputs?
- What external dependencies need mocking (database, auth, APIs)?
- What error conditions and edge cases (empty, null, boundaries) must be handled?
- Is this a pure function or does it have side effects / auth checks?
- Are there coverage targets for this module?
1---2name: unit-testing3description: Write unit tests with Vitest for TypeScript projects — mocking, async testing, parameterized tests, server action testing, and coverage. Use when writing unit tests for utilities, server-side logic, schemas, or pure functions.4---56# Unit Testing Skill (Vitest)78Write fast, isolated unit tests with **Vitest** for TypeScript logic — utilities, Zod schemas, hooks,9and Server Actions (with dependencies mocked). Unit tests do NOT render components in a browser10(use the `storybook-testing` skill for that).1112This file holds the rules. Code lives in the references:1314> - [references/examples.md](./references/examples.md) — seven worked examples: pure utility, Zod schema,15> Server Action with mocked DB, transform function, parameterized (`it.each`), async error handling, hooks.16> - [references/patterns.md](./references/patterns.md) — AAA, test isolation, mock boundaries, the17> `vi.mock` vs `vi.fn` vs `vi.spyOn` decision table, coverage targets, and anti-patterns.18> - [references/mocking.md](./references/mocking.md) — the full mocking catalogue (factories, hoisting,19> partial/dynamic mocks, async, DB/auth/time/env recipes). Read before writing non-trivial mocks.2021## Global Test Utilities2223This project enables **global test utilities** — do **not** import them:2425```typescript26// ❌ Not needed27import { describe, test, expect, vi, beforeEach } from "vitest";2829// ✅ Available globally: describe, test (= it), expect, beforeEach/afterEach/beforeAll/afterAll, vi30```3132Configured via `globals: true` in `vitest.config.ts` and `"types": ["vitest/globals"]` in `tsconfig.json`.3334## Workflow35361. **Analyze the code** — inputs, outputs, side effects, dependencies, and every branch.372. **Identify cases** — happy path, edge cases, error paths, boundary values.383. **Write tests** — AAA pattern (Arrange, Act, Assert); mock external dependencies at module boundaries.394. **Run** — `npm run test:unit`; verify all pass and check coverage.4041## File Naming4243Tests live **next to the source file**, named `<source-filename>.test.ts` (`.test.tsx` if it imports44React/JSX):4546```47src/utils/format-currency.ts → src/utils/format-currency.test.ts48src/features/budgets/actions/create-budget.ts → .../create-budget.test.ts49```5051## Core Rules5253- **AAA structure** — Arrange, Act, Assert; group with nested `describe` (module → function → case).54- **Test behavior, not implementation** — assert on return values and observable effects, never on55 private internals; don't test private functions or abuse snapshots.56- **Mock only at module boundaries** — external dependencies (DB, auth, network, time, UUID). Never57 mock the internal utility you're testing.58- **Pick the right mock tool** (full decision table in [patterns.md](./references/patterns.md#when-to-use-vimock-vs-vifn-vs-vispyon)):59 - `vi.mock("module", factory)` — replace an entire module (DB, auth).60 - `vi.fn()` — a standalone mock (callback / injected dependency).61 - `vi.spyOn(obj, "method")` — observe or replace one method, restorable via `.mockRestore()`.62- **Reset between tests** — `beforeEach(() => vi.clearAllMocks())`; `afterEach(() => vi.restoreAllMocks())`.63- **Type your mocks** — `vi.mocked(fn).mockResolvedValue(...)`.64- **Parameterize repetitive cases** with `test.each([...])` instead of copy-pasting tests.65- **Async** — `await` the call; use `await expect(p).rejects.toThrow(...)` / `.resolves.toMatchObject(...)`.66- **Pure functions need no mocking** — the easiest and highest-value tests.6768## Mocking6970The three core tools (`vi.fn`, `vi.mock`, `vi.spyOn`) are summarized above. Two more worth knowing:71`vi.hoisted(() => ({ ... }))` when a `vi.mock` factory needs shared mock references, and72`vi.mock(import("./m"), async (orig) => ({ ...(await orig()), one: vi.fn() }))` for partial mocks that73keep the real exports.7475> The full catalogue — async resolution, hoisting edge cases, third-party libraries, and database/auth76> recipes — is in [references/mocking.md](./references/mocking.md).7778## Setup (vitest.config.ts essentials)7980```typescript81test: {82 globals: true, // no test-utility imports needed83 environment: "node", // "jsdom" for hooks/DOM84 include: ["src/**/*.test.{ts,tsx}"],85 coverage: {86 provider: "v8",87 exclude: ["src/**/*.test.{ts,tsx}", "src/**/*.stories.{ts,tsx}", "src/**/index.ts", "src/types/**"],88 },89}90```9192## Running Tests9394```bash95npm run test:unit # all unit tests96npm run test:unit -- --watch # watch mode97npm run test:unit -- src/utils/format-currency.test.ts # one file98npm run test:unit -- --grep "formatCurrency" # by pattern99npm run test:unit -- --coverage # coverage report100```101102## Questions to Ask103104- What are the function's inputs and expected outputs?105- What external dependencies need mocking (database, auth, APIs)?106- What error conditions and edge cases (empty, null, boundaries) must be handled?107- Is this a pure function or does it have side effects / auth checks?108- Are there coverage targets for this module?