Fix errors · Skill
Write unit tests with Jest
write-unit-tests-with-jest
Writes and maintains Jest tests in projects that already use Jest - ts-jest, Babel or next/jest setup, jest.mock hoisting, spies, fake timers, mock resets, the ESM transform caveat, snapshots and coverage thresholds. Use when the repo has a jest.config, the user asks for Jest tests or jest.mock help, a Jest run is flaky, or they use Next.js, React Native or NestJS with Jest.
Works with: Any AI Not yet model-tested; testing Jan 2027
Paste it into ChatGPT, Claude, Gemini or any AI chat, then tell it about your project. How it works
Install in Claude Code, Cursor or Codex (optional)
Only for AI coding tools that read skill folders. Run one line in a terminal, in your project folder. It downloads this skill's text files from TheSkillVault and nothing else; you can read them first with “View raw SKILL.md”.
Claude Code (all your projects)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/raw' -o ~/'.claude/skills/write-unit-tests-with-jest/SKILL.md' && curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/files/LICENSE.txt' -o ~/'.claude/skills/write-unit-tests-with-jest/LICENSE.txt'Cursor (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/raw' -o '.cursor/skills/write-unit-tests-with-jest/SKILL.md' && curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/files/LICENSE.txt' -o '.cursor/skills/write-unit-tests-with-jest/LICENSE.txt'Codex and other Agent Skills tools (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/raw' -o '.agents/skills/write-unit-tests-with-jest/SKILL.md' && curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/write-unit-tests-with-jest/files/LICENSE.txt' -o '.agents/skills/write-unit-tests-with-jest/LICENSE.txt'
Or connect the whole library over MCP
Add TheSkillVault to Cursor or add this to your app's MCP settings (read-only; help):
{
"mcpServers": {
"theskillvault": {
"url": "https://theskillvault.ai/mcp"
}
}
}Jest is the long-standing JavaScript test runner. Many React, React Native, Next.js and NestJS projects already use it. If a project has Jest set up, keep using it rather than switching tools.
When to use
- The repo already has
jest.config.*or ajestkey in package.json. - You need tests for functions, services or React components in a Jest project.
- A Jest mock is not working, or a test is flaky.
- You want coverage thresholds enforced in CI.
When not to use
- A new Vite-based project with no runner yet. Use write-unit-tests-with-vitest instead.
- End-to-end browser flows. Use test-web-app-with-playwright.
Steps
- Check what exists: run
npx jest --versionand read the current config before adding anything. - Pick a setup path if Jest is missing (see "Setup").
- Put tests next to code as
name.test.ts, or in__tests__/. Follow whichever the repo already uses. - Write one behaviour per test. Name it as a sentence:
it("rejects an expired coupon"). - Arrange, act, assert. Keep setup visible in the test or in a small helper.
- Mock only what crosses a boundary: network, time, randomness, file system.
- Reset mocks between tests (see "Mocking").
- Run
npx jest --watchwhile working, then the full suite once. - Add coverage thresholds and run
jest --ciin your pipeline.
Setup
| Project | Setup | | --- | --- | | TypeScript, Node | `npm i -D jest ts-jest @types/jest` then `npx ts-jest config:init` | | Babel already present | `npm i -D jest babel-jest`, add `@babel/preset-typescript` if needed | | Next.js | `npm i -D jest jest-environment-jsdom` and use `next/jest` in the config | | React Native | the template ships with Jest and the `react-native` preset |
// jest.config.js (Next.js)
const nextJest = require("next/jest");
const createJestConfig = nextJest({ dir: "./" });
module.exports = createJestConfig({
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1" },
});
ts-jest type-checks while testing, which is slower but catches errors. babel-jest only strips types, so run tsc --noEmit separately.
Matchers worth knowing
toBefor primitives,toEqualfor objects and arrays,toStrictEqualto also check undefined keys.toThrow(/expired/)with a function wrapper:expect(() => apply(code)).toThrow(/expired/).toHaveBeenCalledWith(...)andtoHaveBeenCalledTimes(n)for mocks.expect.objectContaining({ id: 1 })andexpect.any(Date)for partial checks.
Async code
it("loads the user", async () => {
await expect(getUser(1)).resolves.toMatchObject({ id: 1 });
});
it("fails for a missing user", async () => {
await expect(getUser(999)).rejects.toThrow("not found");
});
Always await or return the promise. A test that forgets passes before the assertion runs.
Mocking
import { sendEmail } from "./mailer";
import { registerUser } from "./register";
jest.mock("./mailer"); // hoisted above the imports by Jest
const mockedSend = jest.mocked(sendEmail);
beforeEach(() => jest.clearAllMocks());
it("sends a welcome email", async () => {
mockedSend.mockResolvedValue({ ok: true });
await registerUser({ email: "a@example.com" });
expect(mockedSend).toHaveBeenCalledWith(expect.objectContaining({ to: "a@example.com" }));
});
jest.mockcalls are hoisted. Variables used inside the factory must start withmock.jest.spyOn(obj, "method")wraps one method and keeps the rest real. CallmockRestore()or setrestoreMocks: truein config.clearAllMocksresets call history,resetAllMocksalso removes implementations,restoreAllMocksputs originals back.- For time, use
jest.useFakeTimers(), thenjest.advanceTimersByTime(1000)orjest.setSystemTime(new Date("2026-01-01")). Calljest.useRealTimers()after.
ES modules
Jest's native ESM support is still experimental. If your package uses "type": "module", either let ts-jest or Babel transform to CommonJS, or run node --experimental-vm-modules node_modules/.bin/jest and use jest.unstable_mockModule with dynamic import(). Packages that ship only ESM often need adding to transformIgnorePatterns.
React components
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";
it("increments when clicked", async () => {
render(<Counter />);
await userEvent.setup().click(screen.getByRole("button", { name: /add/i }));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
Add import "@testing-library/jest-dom" in your setup file for matchers like toBeInTheDocument. Query by role and label, as a user would, not by class names.
Snapshots
Use small inline snapshots (toMatchInlineSnapshot()) for stable output such as formatted strings. Avoid snapshotting whole component trees: they break on every markup change and people update them without reading.
Coverage and CI
// jest.config.js
coverageThreshold: { global: { branches: 80, functions: 80, lines: 80 } },
Run jest --ci --coverage in the pipeline. --ci fails on missing snapshots instead of writing them.
Gotchas
- Shared state between tests causes flaky order-dependent failures. Reset in
beforeEach. - "Cannot use import statement outside a module" means an ESM file is not transformed. Check
transformandtransformIgnorePatterns. - Path aliases from tsconfig need a matching
moduleNameMapper. - Tests should never call real APIs or use real secrets, tokens or passwords. Load fake values from a test
.envfile. - Open handles (servers, DB pools) keep Jest running. Close them in
afterAll, and use--detectOpenHandlesto find them.
Portability
The API matches Vitest closely, so these patterns work with Vitest too if you migrate later. Testing Library works the same with any framework adapter, such as Vue or Svelte.
Example prompt
"Add Jest tests for src/billing/applyCoupon.ts. Mock the coupon API, cover expired and invalid codes, and make sure mocks reset between tests."
Done when
- New tests pass locally and with
jest --ci. - Each test checks one behaviour and passes when run on its own.
- Mocks are reset between tests and no test touches the network.
- Coverage thresholds are set and enforced in CI.
---
Adapted from "javascript-typescript-jest" in github/awesome-copilot (MIT, Copyright GitHub, Inc.). See LICENSE.txt.
Bundled files
Licence
Adapted from Aaron Powell (github/awesome-copilot, javascript-typescript-jest); edits by jamesmontemagno (Copyright GitHub, Inc.), MIT License. Expanded and rewritten by TheSkillVault on 2026-11-02: the 44-line upstream checklist became a full guide with setup paths, config, async, mocking (hoisting, spies, fake timers, resets), the ESM caveat, Testing Library, snapshots, coverage and CI, with original examples, gotchas and done-when. Not the original file.
Licence: MIT License. Attribution when you reuse it: Based on work by Aaron Powell (github/awesome-copilot, javascript-typescript-jest); edits by jamesmontemagno (Copyright GitHub, Inc., MIT License), https://github.com/github/awesome-copilot/tree/5269f7ea2d6c3a9bfda5ed878b6726cf509121fd/skills/javascript-typescript-jest; adapted by TheSkillVault, https://theskillvault.ai