Jest Testing skill for Codex
Mocks that reset, timers that advance, Jest that exits. Codex sets up and repairs Jest suites with the details that usually cost an afternoon: which mock-reset flags to enable, why a factory mock returns undefined, when to use the async timer APIs, how to run native ESM, and what changed in Jest 30. It fixes open handles at the source instead of reaching for --forceExit.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- jest-testing Installs to ~/.agents/skills/jest-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install jest-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project jest-testing Installs to .agents/skills/jest-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/jest-testing.zipunzip -o jest-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) jest-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $jest-testing, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures and writes Jest 29/30 test suites - jest.config, transforms (babel-jest, ts-jest, @swc/jest), module mocks and spies, fake timers, ESM with --experimental-vm-modules and jest.unstable_mockModule, coverage thresholds and CI sharding. Use when tests run under Jest, when jest.mock does not take effect, timers or promises hang, "Cannot use import statement outside a module" appears, open handles keep Jest alive, or a Jest 30 upgrade breaks the suite.
Use it when
- The project already runs its tests on Jest
- Mocks leak between tests or do not take effect
- ESM imports fail under Jest
- Jest does not exit or reports open handles
Use something else when
- The project uses Vite or you are choosing a runner for a new TS project: use vitest-testing
- The question is about querying rendered React components: use react-testing-library
How to check the result
The suite passes both with --runInBand and in parallel, exits without --forceExit, and a deliberately broken mocked behavior makes a test fail.
The mistake it prevents
Enabling resetMocks globally, which silently wipes implementations defined in jest.mock factories so mocked functions start returning undefined.
A healthy Jest suite starts fast, isolates mocks per test automatically, never waits on
real timers, and exits on its own without --forceExit.
Inspect first
- Read
AGENTS.mdandpackage.jsonscripts: the real test command may add flags (--runInBand,--selectProjects, env vars). - Find the config:
jest.config.{js,ts,mjs,cjs}or the"jest"key inpackage.json. NotetestEnvironment,transform,moduleNameMapper,setupFilesAfterEnv,projects, and the mock-reset flags. - Check the installed version:
npx jest --version. Jest 30 removed matcher aliases and renamed--testPathPatternto--testPathPatterns. - Check
"type": "module"inpackage.jsonandtsconfig.jsonmodulesetting; they decide the ESM path below. - If the project is Vite-based and has no Jest investment yet, recommend Vitest instead
(
$vitest-testing); do not start a new Jest setup there.
Baseline config
// jest.config.ts (loading a TS config needs ts-node; otherwise use jest.config.mjs)
import type { Config } from "jest";
const config: Config = {
testEnvironment: "node", // "jsdom" only for DOM tests; install jest-environment-jsdom
transform: { "^.+\\.(t|j)sx?$": ["@swc/jest", {}] },
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"\\.(css|scss)$": "identity-obj-proxy",
},
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"],
restoreMocks: true, // restore spies and reset mocks before each test
clearMocks: true,
coverageProvider: "v8",
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/**/index.ts"],
coverageThreshold: { global: { branches: 75, lines: 80 } },
};
export default config;
Transform choice
| Need | Use |
|---|---|
| Fast TS/JSX, no type checking in tests | @swc/jest |
| Type errors must fail tests | ts-jest (slower; or run tsc --noEmit separately and keep swc) |
| Existing Babel config (React Native, legacy) | babel-jest (default when a Babel config exists) |
Run tsc --noEmit in CI regardless; transforms that strip types do not check them.
Mocking
Module mocks
jest.mock() calls are hoisted above imports by the transform. Factories may only reference
variables whose names start with mock.
import { sendEmail } from "../src/mailer";
import { registerUser } from "../src/register";
jest.mock("../src/mailer", () => ({
sendEmail: jest.fn().mockResolvedValue({ id: "msg_1" }),
}));
it("sends a welcome email after registration", async () => {
await registerUser({ email: "[email protected]" });
expect(sendEmail).toHaveBeenCalledTimes(1);
expect(sendEmail).toHaveBeenCalledWith(expect.objectContaining({ to: "[email protected]" }));
});
Partial mock, keeping the rest real:
jest.mock("../src/config", () => ({
...jest.requireActual("../src/config"),
featureFlags: { newCheckout: true },
}));
Spies
const spy = jest.spyOn(paymentGateway, "charge").mockResolvedValueOnce({ ok: false });
With restoreMocks: true the spy is undone before the next test. Without it, call
jest.restoreAllMocks() in afterEach, or a spy from one test leaks into the next.
| Flag | Effect before each test |
|---|---|
clearMocks | wipes mock.calls and results |
resetMocks | also removes implementations (factory mockResolvedValue is lost) |
restoreMocks | restores original methods behind jest.spyOn |
Gotcha: resetMocks: true erases implementations set inside jest.mock factories, so tests
that relied on them suddenly get undefined. Prefer clearMocks + restoreMocks and set
per-test behavior with mockResolvedValueOnce.
Typing mocks
import { fetchUser } from "../src/api";
jest.mock("../src/api");
const mockedFetchUser = jest.mocked(fetchUser);
mockedFetchUser.mockResolvedValue({ id: "u1", name: "Ada" });
Fake timers
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it("retries after 2 seconds", async () => {
const promise = fetchWithRetry(url);
await jest.advanceTimersByTimeAsync(2000); // flushes promises between timer callbacks
await expect(promise).resolves.toEqual(data);
});
it("stamps the current date", () => {
jest.setSystemTime(new Date("2026-03-01T12:00:00Z"));
expect(makeInvoice().issuedAt).toBe("2026-03-01");
});
- Use the
...Asynctimer functions whenever the code awaits between timers; the sync versions leave promise callbacks unrun and the test hangs or times out. jest.runAllTimers()loops forever onsetInterval; userunOnlyPendingTimers().- Libraries that capture
setTimeoutat import time are not faked; enable fake timers before importing them or inject a scheduler.
ESM
Pick one path and state it in the config comments.
| Situation | Path |
|---|---|
| Source is TS/ESM syntax but compiled by a transform | Keep CJS output from the transform; nothing special |
Dependency in node_modules ships ESM only | Add it to transformIgnorePatterns exceptions so it is transformed |
Project runs native ESM ("type": "module", no transform) | Run Jest with --experimental-vm-modules |
// transformIgnorePatterns exception for ESM-only packages
transformIgnorePatterns: ["/node_modules/(?!(nanoid|uuid|@scope/esm-lib)/)"],
{ "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" } }
In native ESM, jest.mock is not hoisted. Mock first, then import dynamically:
import { jest } from "@jest/globals";
jest.unstable_mockModule("../src/mailer.js", () => ({ sendEmail: jest.fn() }));
const { sendEmail } = await import("../src/mailer.js");
const { registerUser } = await import("../src/register.js");
If ESM pain dominates the work, say so and propose migrating the suite to Vitest rather than stacking workarounds.
Running and debugging
npx jest path/to/file.test.ts -t "sends a welcome email" # one test
npx jest --testPathPatterns checkout # Jest 30 flag name
npx jest --onlyChanged # files touched since last commit
npx jest --findRelatedTests src/cart.ts src/tax.ts # tests that import these
npx jest --runInBand --detectOpenHandles # find what keeps Jest alive
npx jest --ci --shard=2/4 --coverage # CI: no snapshot writes, one shard
--cimakes missing snapshots fail instead of being written. Always use it in CI.- “Jest did not exit one second after the test run” means open handles: close servers,
DB pools and timers in
afterAll. Do not add--forceExitas the fix. - Memory growth across a big suite: try
--workerIdleMemoryLimit=512MBand look for module-level caches.
Jest 30 upgrade checklist
- Node 18+ and TypeScript 5.4+ for type definitions
- Replace removed matcher aliases (
toBeCalled->toHaveBeenCalled,toThrowError->toThrow,lastCalledWith->toHaveBeenLastCalledWith);eslint-plugin-jestcan autofix these - Rename
--testPathPatternto--testPathPatternsin scripts and CI - Re-run snapshots with
--cifirst to see real diffs before updating any - Check that newly matched
.mts/.ctsfiles are intended tests
Gotchas
jest.mockpaths are resolved relative to the test file, and must match how the unit imports the module (an alias import and a relative import can resolve to the same file but a mismatch inmoduleNameMappercreates two module instances).- Mocking a module after it has been imported by another module in the same file has no
effect; use
jest.isolateModulesor reorder. expect(asyncFn()).rejects...withoutawaitpasses silently. Alwaysawait expect(...).testEnvironment: "jsdom"for the whole repo slows pure logic tests; use a per-file docblock/** @jest-environment jsdom */orprojects.
Verification
- Run the target file, then the full suite with the project command.
- Run the changed tests with
--runInBandand again in parallel to expose shared state. - Confirm the process exits without
--forceExitand--detectOpenHandlesis silent. - For mock changes, temporarily break the mocked behavior and confirm a test fails.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Jest Testing"
short_description: "Mocks that reset, timers that advance, Jest that exits"
default_prompt: "$jest-testing jest.mock isn't replacing my module, the real one still runs" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.