---
name: write-unit-tests-with-jest
description: 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.
license: MIT
compatibility: Needs Node 18+
metadata:
  category: debugging
  tags: "testing,nodejs,react-native"
  author: johnny-bravo
  version: "1.0"
  updated: "2026-11-02"
  status: published
  aliases: "jest, jest.mock, ts-jest, jest config, jest snapshot, react native tests"
  source_url: "https://github.com/github/awesome-copilot/tree/5269f7ea2d6c3a9bfda5ed878b6726cf509121fd/skills/javascript-typescript-jest"
  source_license: MIT
  copyright: "Copyright GitHub, Inc."
  original_author: "Aaron Powell (github/awesome-copilot, javascript-typescript-jest); edits by jamesmontemagno"
  modified: "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."
  imported_on: "2026-11-02"
---

# Write unit tests with Jest

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 |

```js
// 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
```ts
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
```ts
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
```tsx
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
```js
// 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.
