Jest Best Practices
Why do tests pass locally but fail in CI?
Bad config is the root cause. State leaks between suites. --forceExit masks open handles. Slow runs kill feedback loops. This page is the checklist you scan when something breaks.
Configuration
The things that burn you.
| Setting | What | Why |
|---|---|---|
jest.preset.ts | Shared root config | One source of truth for transform, moduleFileExtensions, testEnvironment |
| Per-project config | extends the preset | Override only what differs (testMatch, setupFiles) |
isolatedModules: true | ts-jest skips type checking | 2-5x faster transforms — run tsc separately |
moduleNameMapper | Must match bundler aliases exactly | Mismatches cause "Cannot find module" that only shows in test |
testMatch vs --testPathPattern | testMatch in config, testPathPattern CLI only | Mixing them creates silent test skipping |
transformIgnorePatterns | Allowlist untranspiled ESM packages | Default ignores all of node_modules — ESM packages need transforming |
jest --showConfig | Validate merged config | Run this first when debugging — shows exactly what Jest sees |
// jest.preset.ts — shared root
export default {
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': ['ts-jest', { isolatedModules: true }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
};
// libs/my-lib/jest.config.ts — per project
import preset from '../../jest.preset';
export default {
...preset,
displayName: 'my-lib',
testMatch: ['<rootDir>/src/**/*.spec.ts'],
};
Test Isolation
Stop state leaks.
afterEach: usejest.restoreAllMocks()—clearMocksonly resets call history,resetMocksreplaces withjest.fn()but loses implementation,restoreAllMocksreturns the original- DB tests: wrap each test in a transaction, rollback in
afterEach— truncate if transactions are impractical - Timer mocks:
jest.useFakeTimers()inbeforeEach,jest.useRealTimers()inafterEach - Never seed in
beforeAll— shared state means test order determines pass/fail workerIdleMemoryLimit: set to512MBto kill workers that leak memory between suites
// Correct mock restoration
afterEach(() => {
jest.restoreAllMocks();
});
// DB isolation via transaction rollback
let tx: Transaction;
beforeEach(async () => {
tx = await db.transaction();
});
afterEach(async () => {
await tx.rollback();
});
Mock Comparison
| Method | Clears calls | Resets implementation | Restores original |
|---|---|---|---|
clearMocks | Yes | No | No |
resetMocks | Yes | Yes (to jest.fn()) | No |
restoreAllMocks | Yes | Yes | Yes |
Performance
Stop waiting.
--runInBandper project — Nx parallelizes across projects, Jest parallelizes within;runInBandavoids double-parallelism overheadmaxWorkers: '50%'local,25%in watch mode — full cores thrash the OSnx affected:test— only run what the changeset touches- Domain configs in
project.json—testPathPatternper domain for single-suite runs test:quicktargets — skipdependsOnfor iterative dev, run full chain in CI
// project.json — domain isolation
{
"targets": {
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "libs/my-lib/jest.config.ts",
"runInBand": true
},
"configurations": {
"auth": { "testPathPattern": "auth/" },
"billing": { "testPathPattern": "billing/" }
}
}
}
}
Nx Integration
| Pattern | Config | Purpose |
|---|---|---|
| Domain isolation | configurations with testPathPattern | Run one domain without touching others |
| Infra dependencies | dependsOn: ["migrate", "seed"] | DB migrations run before test suite |
| Quick targets | test:quick without dependsOn | Skip infra for iterative red-green cycles |
| Cache inputs | nx.json targetDefaults with inputs | Only invalidate cache when source or config changes |
// nx.json — cache correctly
{
"targetDefaults": {
"test": {
"inputs": [
"{projectRoot}/src/**/*",
"{projectRoot}/jest.config.ts",
"{workspaceRoot}/jest.preset.ts"
],
"cache": true
}
}
}
CI
Stop flaky builds.
- Never ship
--forceExit— use--detectOpenHandlesto find the leak, then fix it - Coverage: use
v8provider (faster thanbabel), set thresholds per critical module - Sharding:
--shard=1/4in a CI matrix strategy — linear speedup - Remote caching: Nx Cloud skips suites that haven't changed since last green run
# GitHub Actions matrix sharding
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- run: npx nx affected:test -- --shard=${{ matrix.shard }}
// jest.config.ts — coverage
export default {
coverageProvider: 'v8',
coverageThreshold: {
'./src/core/': {
branches: 80,
functions: 80,
lines: 80,
},
},
};
Common Pitfalls
| Pitfall | Why it hurts | Fix |
|---|---|---|
--forceExit | Masks open DB connections, timers, server handles | --detectOpenHandles, fix the leak |
beforeAll DB seed | Shared mutable state, test order matters | Seed in beforeEach or transaction rollback |
Missing await in cleanup | Cleanup runs after next test starts | Always await in afterEach |
Top-level jest.mock() | Bleeds across all tests in the file | Mock inside describe or beforeEach |
| Snapshot overuse | Tests pass forever — you stop reading diffs | Test behavior and assertions, not markup |
clearMocks instead of restoreAllMocks | Spied implementations persist, cause phantom passes | Always restoreAllMocks |
| Hardcoded ports in test servers | Parallel workers collide | Use port 0 (OS assigns free port) |
setTimeout in tests | Flaky, slow, hides async bugs | jest.useFakeTimers() + jest.advanceTimersByTime() |
Context
- Testing Stack — Testing tools and patterns overview
- React Testing Library — Component testing with RTL
- Flow Engineering — How tests fit the delivery pipeline
- Type-First Development — Types before tests before implementation
Links
- Jest Documentation — Official config reference
- goldbergyoni — JavaScript Testing Best Practices — 50+ patterns with examples
- Nx Jest Plugin — Executor options and cache configuration
- Kent C. Dodds — Testing Trophy — Integration tests give the best ROI
- Kent C. Dodds — Common Mistakes with React Testing Library — Patterns that look right but aren't
Questions
How do you know your test suite is catching real bugs versus just passing?
- What's the cost of a test that passes with stale mocks — and how would you detect it?
- If
--forceExitdisappeared tomorrow, how many suites would fail — and what does that number tell you about your cleanup discipline? - When does test isolation become test duplication — where's the line between safety and waste?
- What should a test suite's execution time budget be, and what happens to team behavior when it's exceeded?