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 a jest key 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

  1. Check what exists: run npx jest --version and read the current config before adding anything.
  2. Pick a setup path if Jest is missing (see "Setup").
  3. Put tests next to code as name.test.ts, or in __tests__/. Follow whichever the repo already uses.
  4. Write one behaviour per test. Name it as a sentence: it("rejects an expired coupon").
  5. Arrange, act, assert. Keep setup visible in the test or in a small helper.
  6. Mock only what crosses a boundary: network, time, randomness, file system.
  7. Reset mocks between tests (see "Mocking").
  8. Run npx jest --watch while working, then the full suite once.
  9. Add coverage thresholds and run jest --ci in 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

  • toBe for primitives, toEqual for objects and arrays, toStrictEqual to also check undefined keys.
  • toThrow(/expired/) with a function wrapper: expect(() => apply(code)).toThrow(/expired/).
  • toHaveBeenCalledWith(...) and toHaveBeenCalledTimes(n) for mocks.
  • expect.objectContaining({ id: 1 }) and expect.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.mock calls are hoisted. Variables used inside the factory must start with mock.
  • jest.spyOn(obj, "method") wraps one method and keeps the rest real. Call mockRestore() or set restoreMocks: true in config.
  • clearAllMocks resets call history, resetAllMocks also removes implementations, restoreAllMocks puts originals back.
  • For time, use jest.useFakeTimers(), then jest.advanceTimersByTime(1000) or jest.setSystemTime(new Date("2026-01-01")). Call jest.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 transform and transformIgnorePatterns.
  • 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 .env file.
  • Open handles (servers, DB pools) keep Jest running. Close them in afterAll, and use --detectOpenHandles to 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