Claude Code with Jest: Mocks, Coverage, ESM Workflow
Why Jest needs a project-specific CLAUDE.md
Jest is the most-used JavaScript test runner in 2026, and that breadth is the problem. Half the Jest content on the public web is from the Babel-only era. A quarter assumes plain JavaScript. The remainder splits between ts-jest, swc, and the ESM workarounds that have shifted across Jest 28, 29, and 30. Claude Code has read all of it, so the default test code it writes is a Frankenstein of patterns from different stacks.
Without a project-specific CLAUDE.md, Claude mixes jest.mock and jest.spyOn interchangeably, reaches for done callbacks instead of await, generates snapshot tests for unstable React output, and hands you a jest.config.ts using a transformer combination your project does not actually run.
This guide covers the CLAUDE.md rules that make Claude Code reliable for Jest. If you are new to the broader workflow, the Claude Code setup guide covers installation, and the Claude Code testing guide covers test strategy across runners.
The Jest CLAUDE.md template
The CLAUDE.md at your project root is read before every Claude Code session. For Jest, it needs to answer: which version, which transformer, ESM or CJS, mocking conventions, coverage thresholds, and async patterns.
# Jest project rules
## Stack
- Jest: 30.x
- Transformer: @swc/jest (do not switch to ts-jest or babel-jest without approval)
- TypeScript: 5.6.x with strict mode and isolatedModules
- Module system: ESM (type: "module" in package.json)
- Node: 20.x with --experimental-vm-modules flag in test script
- Testing libraries: @testing-library/react 16.x, @testing-library/jest-dom 6.x
- Test environment: jsdom for component tests, node for unit/integration tests
## Project structure
- src/: application source
- src/__tests__/: integration tests (one folder per feature)
- src/**/__tests__/*.test.ts: co-located unit tests
- src/__mocks__/: manual module mocks (matched by jest.mock automatically)
- jest.config.ts: single config file, no projects array unless monorepo
- jest.setup.ts: global test setup (matchers, environment)
## Jest syntax rules
- Use describe/it, never describe/test, for consistency across the codebase
- Co-locate unit tests next to source files in __tests__/ subfolders
- One assertion per it block where possible. Multi-assertion blocks need a comment
- Always use async/await for async tests. NEVER use done callbacks
- NEVER use jest.useFakeTimers() without an explicit jest.useRealTimers() in afterEach
## Mocking rules
- jest.mock() for entire modules at the top of the file (hoisted)
- jest.spyOn() for individual methods on existing objects (preserves original)
- NEVER mix jest.mock and jest.requireActual without a clear reason in a comment
- Reset mocks between tests: clearMocks: true in jest.config.ts (already set)
- For React components, prefer @testing-library/react user events over jest.fn() handlers
## Coverage thresholds
- Statements: 80%
- Branches: 75%
- Functions: 80%
- Lines: 80%
- Excluded from coverage: src/types/, src/generated/, *.stories.tsx, *.config.ts
## Hard rules
- NEVER commit a snapshot file that contains unstable values (Date.now, random IDs)
- NEVER use --forceExit to fix open handles. Find and close the handle
- NEVER skip a failing test with .skip or xit without a linked issue in a comment
- NEVER mock React, react-dom, or @testing-library/* (they are runtime dependencies)
- NEVER write a test that asserts implementation details (private methods, internal state)
Three rules in this template prevent the most common Claude Code failures.
The mocking rule matters because jest.mock and jest.spyOn solve different problems. jest.mock replaces the entire module before imports run. jest.spyOn wraps a single method on an already-imported object. Without the rule, Claude reaches for jest.mock even when the goal is to observe a real method, producing tests that pass but no longer exercise the real code path.
The async rule kills the largest class of flaky tests. done callbacks predate async/await and are still everywhere in Stack Overflow answers. They allow a test to pass if the assertion runs before done is called and fail intermittently when timing shifts. async/await makes the test deterministic.
The forceExit rule is the most boring and most important. --forceExit makes Jest kill the process after the test run regardless of dangling timers, sockets, or database connections. It hides bugs. Once it is in package.json, every future test session inherits the same blindness.
For TypeScript-specific patterns, the Claude Code TypeScript guide covers tsconfig strictness rules that pair with Jest's isolatedModules requirement.
jest.config.ts patterns that work
Once CLAUDE.md is in place, give Claude a real config to extend. A reference config lets it pattern-match against your actual transformer setup instead of generating from scratch.
// jest.config.ts
import type { Config } from 'jest'
const config: Config = {
preset: undefined,
testEnvironment: 'jsdom',
setupFilesAfterEach: ['<rootDir>/jest.setup.ts'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js
AI Toolz Dir