协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
vitest【开发与工程】Vitest is a test runner for JavaScript and TypeScript built on Vite, with a Jest-compatible API, built-in mocking, snapshots and code coverage. Use when a user asks to set up or run unit tests in a Vite or Node project, write tests with describe/it/expect, mock modules with vi.mock, add coverage thresholds, configure vitest.config.ts or test projects…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Vitest is the Vite-native testing framework. It runs unit, integration and component tests with native TypeScript support, a Jest-compatible API, built-in mocking, code coverage, snapshot testing and watch mode — reusing Vite's transform pipeline, so tests need no separate compilation step. This skill targets Vitest 5 (released September 2026), which changed several defaults; the Guidelines list what to check when upgrading.
npm install -D vitest
npm install -D @vitest/coverage-v8 # Coverage
npm install -D @vitest/ui # Browser UI
npm install -D jsdom # Only for environment: "jsdom"
Vitest 5 needs Node.js 22.12+ and Vite 6.4+. Vite is a peer dependency: npm, pnpm and Bun install it automatically, Yarn needs yarn add -D vitest vite. Add .vitest/ (reports and artifacts) and coverage/ to .gitignore.
// src/pricing.test.ts
import { describe, it, expect } from "vitest";
import { calculateDiscount, formatPrice } from "./pricing";
describe("calculateDiscount", () => {
it("applies percentage discount", () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it("never goes below zero", () => {
expect(calculateDiscount(10, 200)).toBe(0);
});
it.each([
{ price: 100, discount: 10, expected: 90 },
{ price: 50, discount: 50, expected: 25 },
{ price: 200, discount: 0, expected: 200 },
])("$price with $discount% = $expected", ({ price, discount, expected }) => {
expect(calculateDiscount(price, discount)).toBe(expected);
});
});
describe("formatPrice", () => {
it("formats with currency symbol", () => {
expect(formatPrice(29.99, "USD")).toBe("$29.99");
expect(formatPrice(29.99, "EUR")).toBe("€29.99");
});
});
Test files must contain .test. or .spec. in their name (default include: **/*.{test,spec}.?(c|m)[jt]s?(x)).
// src/orders.test.ts
import { describe, it, expect, vi } from "vitest";
import { processOrder } from "./orders";
import { sendEmail } from "./email";
import { chargeCard } from "./payments";
// vi.mock is hoisted above the imports — keep it at the top level of the file
vi.mock("./email", () => ({
sendEmail: vi.fn().mockResolvedValue({ success: true }),
}));
vi.mock("./payments", () => ({
chargeCard: vi.fn().mockResolvedValue({ chargeId: "ch_3PqXk2" }),
}));
describe("processOrder", () => {
// No beforeEach(vi.clearAllMocks) needed: Vitest 5 clears call history before every test
it("charges card and sends confirmation email", async () => {
const order = { userId: "u_481", items: [{ id: "sku_114", qty: 2 }], total: 59.98 };
const result = await processOrder(order);
expect(chargeCard).toHaveBeenCalledWith({ amount: 59.98, userId: "u_481" });
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({ type: "order_confirmation", userId: "u_481" }),
);
expect(result.status).toBe("completed");
});
it("stops on payment failure", async () => {
vi.mocked(chargeCard).mockRejectedValueOnce(new Error("Card declined"));
await expect(processOrder({ userId: "u_481", items: [], total: 0 }))
.rejects.toThrow("Card declined");
expect(sendEmail).not.toHaveBeenCalled();
});
it("spies on a method", () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
console.log("order shipped");
expect(spy).toHaveBeenCalledWith("order shipped");
});
it("controls timers", () => {
vi.useFakeTimers();
const callback = vi.fn();
setTimeout(callback, 5000);
vi.advanceTimersByTime(5000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
});
});
To keep the real implementation and only record calls, pass { spy: true } instead of a factory: vi.mock(import("./payments"), { spy: true }).
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true, // No need to import describe/it/expect
environment: "node", // Or "jsdom" / "happy-dom" for browser APIs
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"], // Also report files no test imports
thresholds: { lines: 80, branches: 75, functions: 80 },
},
include: ["**/*.{test,spec}.{ts,tsx}"],
setupFiles: ["./test/setup.ts"], // The file must exist; drop the line if there is none
},
});
Vitest also reads test from an existing vite.config.ts. With globals: true, add "types": ["vitest/globals"] to compilerOptions in tsconfig.json. A single file can switch environment with a // @vitest-environment jsdom comment at the top of the file.
Several configurations in one run (monorepo packages, node + DOM tests) are declared as projects in the root config:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: { provider: "v8", include: ["src/**/*.ts"] }, // root-only option
projects: [
{ test: { name: "unit", include: ["src/**/*.test.ts"], environment: "node" } },
{ test: { name: "dom", include: ["src/**/*.dom.test.tsx"], environment: "jsdom" } },
"packages/*", // every folder in packages/ is a project
],
},
});
npx vitest # Watch mode; single run in CI or without a TTY
npx vitest run # Single run (CI)
npx vitest run --coverage # With coverage, fails below thresholds
npx vitest --ui # Browser UI — open the printed URL, it carries a token
npx vitest run src/pricing.test.ts # Files whose path contains the filter
npx vitest run -t "applies percentage" # Tests whose full name matches
npx vitest run --project unit # One project
npx vitest related src/pricing.ts --run # Tests that import the given source files
npx vitest run --changed # Tests affected by uncommitted changes
npx vitest run --reporter=junit # Writes .vitest/junit/output.xml
User: "Set up Vitest for our pricing module and make CI fail under 80% line coverage."
npm install -D vitest @vitest/coverage-v8
npm pkg set scripts.test="vitest" scripts.test:ci="vitest run --coverage"
npm run test:ci
With the two test files and the first configuration above (and an existing test/setup.ts), the mocked email.ts and payments.ts never execute, so the totals fall under the threshold and the command exits with code 1:
Test Files 2 passed (2)
Tests 10 passed (10)
% Coverage report from v8
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 71.42 | 100 | 60 | 71.42 |
email.ts | 0 | 100 | 0 | 0 | 1
payments.ts | 0 | 100 | 0 | 0 | 1
-------------|---------|----------|---------|---------|-------------------
ERROR: Coverage for lines (71.42%) does not meet global threshold (80%)
ERROR: Coverage for functions (60%) does not meet global threshold (80%)
The HTML report is written to coverage/index.html.
User: "After bumping vitest to 5 our workspace tests are gone and one file crashes with a vi.mock error."
vitest.workspace.ts is no longer read, and a test.workspace key throws "The test.workspace option was removed in Vitest 4". Move its entries into test.projects (see Configuration) and delete the file.vi.mock / vi.hoisted call out of describe and test callbacks to the top level of the file. Vitest 5 fails the file otherwise:Error: 1 call in "src/orders.test.ts" was defined outside of the module's top level scope:
- vi.mock("./email") at src/orders.test.ts:2:28
npx vitest run --project unit --reporter=default
✓ |unit| src/orders.test.ts (4 tests) 4ms
✓ |unit| src/pricing.test.ts (6 tests) 10ms
Test Files 2 passed (2)
Tests 10 passed (10)
beforeAll now see zero calls (clearMocks is on). Fix the test, or set test.clearMocks: false to restore the old behavior.describe/it/expect API; replace jest.fn/jest.mock with vi.fn/vi.mock when migratingtsc --noEmit in CIvi.hoisted(). Use vi.doMock() when a mock must be registered later, inside a testclearMocks defaults to true: call counts are cleared before each test, implementations stay. mockReset and restoreMocks are still opt-inif (import.meta.vitest) blocks run once test.includeSource lists the files; add define: { "import.meta.vitest": "undefined" } to the build config so bundlers drop them from productiontest.projects replaced vitest.workspace.ts. Inline projects inherit the root config by default in Vitest 5 (extends: false opts out); coverage and reporters can only be set at the rootcoverage.include to see untested files. In Vitest 5 a pattern without wildcards ("src") is treated as a directory, and glob thresholds need their own perFile.vitest/ — The json and junit reporters write .vitest/json/output.json and .vitest/junit/output.xml instead of stdout unless outputFile is setminimal reporter, which prints only failures and the totals. Pass --reporter=default (or verbose) to see every file--config ../vitest.config.tsnpx vitest init browser)