Comparisons

Jest vs Vitest: AI Rules for Each Test Runner

Jest and Vitest have similar APIs but different configuration, mocking, and performance characteristics. Each needs specific AI rules to prevent the AI from mixing patterns. Side-by-side rules comparison with templates.

7 min read·April 23, 2025

jest.fn() in a Vitest project = ReferenceError — the APIs look identical but the namespace is wrong

Configuration, imports, mocking namespace, performance, and copy-paste rule templates for each

Similar API, Different Internals

Jest and Vitest look almost identical in test code: describe, it, expect, beforeEach, afterEach — the assertion and structure APIs are the same. Vitest was designed as a Jest-compatible replacement, so migration is often a find-and-replace of imports. But the internals differ: Jest uses a custom module system (jest.mock hoisting, manual mocking conventions). Vitest uses Vite's native ESM and module resolution (vi.mock, no hoisting magic). Without AI rules, the AI mixes Jest mock patterns with Vitest APIs — the code looks right but fails at runtime.

The most common AI mistake: generating jest.mock() in a Vitest project (jest is not defined) or vi.mock() in a Jest project (vi is not defined). The second most common: using Jest's __mocks__ directory convention in Vitest (Vitest uses a different mechanism) or assuming Jest's jsdom environment configuration in Vitest (different config format). The API overlap is a trap: the AI sees similar patterns and mixes the wrong tool's specifics.

This article provides: the specific rules needed for each test runner to prevent cross-contamination, configuration differences the AI must know, and copy-paste CLAUDE.md templates. One paragraph of rules prevents the most frustrating testing errors: tests that look correct but fail because the AI used the wrong runner's patterns.

💡 API Overlap Is a Trap

describe, it, expect look identical in both runners. The AI sees familiar patterns and mixes the wrong runner's specifics: jest.mock() in Vitest, vi.fn() in Jest. One namespace rule (vi vs jest) prevents the most frustrating testing errors: code that looks right but fails at runtime.

Configuration: jest.config vs vitest.config

Jest configuration: jest.config.ts (or jest.config.js, package.json jest field). Key settings: testEnvironment (jsdom for browser, node for server), transform (babel-jest or ts-jest for TypeScript), moduleNameMapper (path aliases like @/), setupFilesAfterSetup (test setup, custom matchers). Jest uses its own transform pipeline — separate from the application build tool. AI rule: 'Jest config in jest.config.ts. Use ts-jest for TypeScript transform. testEnvironment: jsdom for component tests, node for utility tests.'

Vitest configuration: vitest.config.ts (or vite.config.ts with test section). Key settings: environment (jsdom, happy-dom, or node), globals (true to use describe/it without import), setupFiles, and resolve.alias (shares Vite's alias configuration — no separate moduleNameMapper). Vitest uses Vite's transform pipeline — the same build tool as the application. AI rule: 'Vitest config in vitest.config.ts. Inherits Vite aliases and transforms. Set globals: true to avoid importing describe/it/expect in every file.'

The configuration rule prevents: AI generating jest.config.ts in a Vitest project (wrong file, wrong format), configuring moduleNameMapper when Vite aliases already work, or adding ts-jest transform when Vite handles TypeScript natively. One rule about which config file to use eliminates an entire class of setup errors.

  • Jest: jest.config.ts, ts-jest for TypeScript, moduleNameMapper for aliases
  • Vitest: vitest.config.ts (or vite.config.ts test section), inherits Vite config
  • Vitest shares Vite transforms and aliases — no separate configuration needed
  • Jest has its own transform pipeline — separate from application bundler
  • Rule prevents: wrong config file, redundant transforms, mismatched aliases

Import Patterns: Globals vs Explicit

Jest with globals (default): describe, it, expect, jest.fn(), jest.mock() are available globally without imports. This is Jest's default behavior. You never write import { describe, it, expect } from 'jest'. The globals are injected by the test runner. AI rule: 'Do not import describe, it, expect, or jest from any module. They are globally available in Jest.'

Vitest with globals (optional): if globals: true is set in vitest.config.ts, describe, it, expect, and vi are available without imports (same as Jest). If globals: false (default): you must import everything: import { describe, it, expect, vi } from 'vitest'. The AI must know which mode the project uses. AI rule: 'If globals enabled: do not import test functions. If globals disabled: import { describe, it, expect, vi } from vitest at the top of every test file.'

The import rule is critical because: AI generating import { describe } from 'jest' fails (Jest does not export these). AI generating bare describe() in a Vitest project with globals: false fails (describe is not defined). The rule tells the AI: which functions are global vs imported, and whether vi or jest is the mock namespace. One import pattern rule prevents the most common test file syntax errors.

Mocking: jest.mock vs vi.mock

Jest mocking: jest.mock('./module') hoists the mock to the top of the file (regardless of where you write it in the code). jest.fn() creates a mock function. jest.spyOn(object, 'method') creates a spy. The factory function: jest.mock('./db', () => ({ query: jest.fn() })). Jest's module mock system is powerful but has hoisting magic that surprises developers (the mock is moved to the top of the file during compilation).

Vitest mocking: vi.mock('./module') also hoists (Vitest replicates Jest's hoisting behavior for compatibility). vi.fn() creates a mock function. vi.spyOn(object, 'method') creates a spy. The factory function: vi.mock('./db', () => ({ query: vi.fn() })). The API is intentionally identical to Jest's — with vi replacing jest. Vitest also supports vi.hoisted() for explicit hoisting control (more transparent than Jest's implicit hoisting).

The mocking rule: 'Use vi.mock (not jest.mock), vi.fn() (not jest.fn()), vi.spyOn (not jest.spyOn). The vi namespace replaces jest for all mocking utilities. Factory functions in vi.mock use vi.fn() for mock implementations.' For Jest: 'Use jest.mock, jest.fn(), jest.spyOn. Mock factories use jest.fn() for implementations. Mocks are hoisted automatically.' The namespace (vi vs jest) is the primary difference — the API shape is identical.

  • Jest: jest.mock(), jest.fn(), jest.spyOn() — hoisted automatically
  • Vitest: vi.mock(), vi.fn(), vi.spyOn() — same API, vi namespace, also hoisted
  • The namespace is the critical rule: vi vs jest determines which runner is in use
  • AI mixing: jest.fn() in a Vitest file = ReferenceError: jest is not defined
  • vi.hoisted() in Vitest: explicit hoisting control, more transparent than Jest
⚠️ Namespace Is the Critical Rule

jest.fn() in a Vitest file: ReferenceError: jest is not defined. vi.mock() in a Jest file: ReferenceError: vi is not defined. The API shape is identical — the namespace determines the runner. One rule ('Use vi namespace, not jest') prevents every mocking error.

Performance and Unique Features

Jest performance: transforms files with babel-jest or ts-jest (compilation per file), runs tests in worker threads, and caches transforms. Cold start: 2-5 seconds (loading Jest, setting up workers). Hot run (cached): 1-3 seconds. Jest's test isolation is strong (each test file runs in a separate worker with its own module registry). AI rule for Jest: 'Tests may run in parallel across files. Do not rely on shared state between test files. Each file has its own module scope.'

Vitest performance: uses Vite's native ESM and HMR for fast test execution. Cold start: 0.5-1 second. Hot run (watch mode with HMR): near-instant (only re-runs affected tests). Vitest can reuse Vite's dev server transform cache. Vitest is 2-10x faster than Jest for typical projects. AI rule for Vitest: 'Tests run via Vite. Inline snapshots update in watch mode. vi.useFakeTimers() for time-dependent tests.'

Vitest unique features: in-source testing (tests inside the source file, stripped in production build), browser mode (run tests in a real browser via Playwright), and benchmarking (bench function for performance benchmarks). Jest unique features: snapshot serializers, custom reporters ecosystem, and the largest community of testing plugins. The AI should know which features are available in the runner being used.

ℹ️ 2-10x Faster Cold Start

Jest: 2-5 second cold start (load runner, compile files). Vitest: 0.5-1 second (reuses Vite transforms, native ESM). Watch mode with HMR: Vitest re-runs affected tests near-instantly. For TDD workflows: the speed difference changes how often you run tests.

Ready-to-Use Rule Templates

Jest CLAUDE.md template: '# Testing (Jest). Test runner: Jest with ts-jest. Config: jest.config.ts. Test files: *.test.ts alongside source files. Globals: describe, it, expect, jest are available without imports. Mocking: jest.mock() for modules, jest.fn() for functions, jest.spyOn() for spies. Setup: jest.setup.ts for custom matchers and global setup. Run: pnpm test (jest), pnpm test:watch (jest --watch). Coverage: pnpm test:coverage (jest --coverage). Do not import describe/it/expect from any package.'

Vitest CLAUDE.md template: '# Testing (Vitest). Test runner: Vitest. Config: vitest.config.ts (inherits from vite.config.ts). Test files: *.test.ts alongside source files. Globals: enabled (describe, it, expect, vi available without imports). Mocking: vi.mock() for modules, vi.fn() for functions, vi.spyOn() for spies. Setup: vitest.setup.ts for custom matchers. Run: pnpm test (vitest), pnpm test:watch (vitest --watch). Coverage: pnpm test:coverage (vitest --coverage). Use vi namespace for all mocking (never jest).'

Copy the template matching your test runner. The template tells the AI: which runner, which config file, which mock namespace, which global functions, and which commands to run. One template paragraph prevents: wrong mock namespace, wrong config format, wrong import patterns, and wrong CLI commands. The most impactful testing rule you can add to any project.

Comparison Summary

Summary of Jest vs Vitest AI rules.

  • Config: jest.config.ts (own pipeline) vs vitest.config.ts (inherits Vite config)
  • Imports: Jest globals by default vs Vitest globals optional (configurable)
  • Mocking: jest.mock/fn/spyOn vs vi.mock/fn/spyOn — same API, different namespace
  • Performance: Jest 2-5s cold start vs Vitest 0.5-1s — Vitest 2-10x faster
  • AI error: jest.fn() in Vitest = ReferenceError. vi.mock() in Jest = ReferenceError
  • Templates: one paragraph per runner prevents all cross-contamination errors
  • Migration: Vitest is API-compatible — replace jest with vi, update config
  • RuleSync: test runner rules in centralized dashboard, synced to CLAUDE.md and .cursorrules