{"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.","category":"debugging","tags":["testing","nodejs","react-native"],"models":["any"],"proven":false,"passing_families":0,"model_testing":"Not yet model-tested; testing Jan 2027","url":"https://theskillvault.ai/skills/write-unit-tests-with-jest","api_url":"https://theskillvault.ai/api/skills/write-unit-tests-with-jest","raw_url":"https://theskillvault.ai/api/skills/write-unit-tests-with-jest/raw","body":"# Write unit tests with Jest\n\nJest 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.\n\n## When to use\n- The repo already has `jest.config.*` or a `jest` key in package.json.\n- You need tests for functions, services or React components in a Jest project.\n- A Jest mock is not working, or a test is flaky.\n- You want coverage thresholds enforced in CI.\n\n## When not to use\n- A new Vite-based project with no runner yet. Use write-unit-tests-with-vitest instead.\n- End-to-end browser flows. Use test-web-app-with-playwright.\n\n## Steps\n1. Check what exists: run `npx jest --version` and read the current config before adding anything.\n2. Pick a setup path if Jest is missing (see \"Setup\").\n3. Put tests next to code as `name.test.ts`, or in `__tests__/`. Follow whichever the repo already uses.\n4. Write one behaviour per test. Name it as a sentence: `it(\"rejects an expired coupon\")`.\n5. Arrange, act, assert. Keep setup visible in the test or in a small helper.\n6. Mock only what crosses a boundary: network, time, randomness, file system.\n7. Reset mocks between tests (see \"Mocking\").\n8. Run `npx jest --watch` while working, then the full suite once.\n9. Add coverage thresholds and run `jest --ci` in your pipeline.\n\n## Setup\n\n| Project | Setup |\n| --- | --- |\n| TypeScript, Node | `npm i -D jest ts-jest @types/jest` then `npx ts-jest config:init` |\n| Babel already present | `npm i -D jest babel-jest`, add `@babel/preset-typescript` if needed |\n| Next.js | `npm i -D jest jest-environment-jsdom` and use `next/jest` in the config |\n| React Native | the template ships with Jest and the `react-native` preset |\n\n```js\n// jest.config.js (Next.js)\nconst nextJest = require(\"next/jest\");\nconst createJestConfig = nextJest({ dir: \"./\" });\nmodule.exports = createJestConfig({\n  testEnvironment: \"jsdom\",\n  setupFilesAfterEnv: [\"<rootDir>/jest.setup.ts\"],\n  moduleNameMapper: { \"^@/(.*)$\": \"<rootDir>/src/$1\" },\n});\n```\n\n`ts-jest` type-checks while testing, which is slower but catches errors. `babel-jest` only strips types, so run `tsc --noEmit` separately.\n\n## Matchers worth knowing\n- `toBe` for primitives, `toEqual` for objects and arrays, `toStrictEqual` to also check undefined keys.\n- `toThrow(/expired/)` with a function wrapper: `expect(() => apply(code)).toThrow(/expired/)`.\n- `toHaveBeenCalledWith(...)` and `toHaveBeenCalledTimes(n)` for mocks.\n- `expect.objectContaining({ id: 1 })` and `expect.any(Date)` for partial checks.\n\n## Async code\n```ts\nit(\"loads the user\", async () => {\n  await expect(getUser(1)).resolves.toMatchObject({ id: 1 });\n});\n\nit(\"fails for a missing user\", async () => {\n  await expect(getUser(999)).rejects.toThrow(\"not found\");\n});\n```\nAlways `await` or return the promise. A test that forgets passes before the assertion runs.\n\n## Mocking\n```ts\nimport { sendEmail } from \"./mailer\";\nimport { registerUser } from \"./register\";\n\njest.mock(\"./mailer\"); // hoisted above the imports by Jest\nconst mockedSend = jest.mocked(sendEmail);\n\nbeforeEach(() => jest.clearAllMocks());\n\nit(\"sends a welcome email\", async () => {\n  mockedSend.mockResolvedValue({ ok: true });\n  await registerUser({ email: \"a@example.com\" });\n  expect(mockedSend).toHaveBeenCalledWith(expect.objectContaining({ to: \"a@example.com\" }));\n});\n```\n- `jest.mock` calls are hoisted. Variables used inside the factory must start with `mock`.\n- `jest.spyOn(obj, \"method\")` wraps one method and keeps the rest real. Call `mockRestore()` or set `restoreMocks: true` in config.\n- `clearAllMocks` resets call history, `resetAllMocks` also removes implementations, `restoreAllMocks` puts originals back.\n- For time, use `jest.useFakeTimers()`, then `jest.advanceTimersByTime(1000)` or `jest.setSystemTime(new Date(\"2026-01-01\"))`. Call `jest.useRealTimers()` after.\n\n## ES modules\nJest'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`.\n\n## React components\n```tsx\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Counter } from \"./Counter\";\n\nit(\"increments when clicked\", async () => {\n  render(<Counter />);\n  await userEvent.setup().click(screen.getByRole(\"button\", { name: /add/i }));\n  expect(screen.getByText(\"Count: 1\")).toBeInTheDocument();\n});\n```\nAdd `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.\n\n## Snapshots\nUse 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.\n\n## Coverage and CI\n```js\n// jest.config.js\ncoverageThreshold: { global: { branches: 80, functions: 80, lines: 80 } },\n```\nRun `jest --ci --coverage` in the pipeline. `--ci` fails on missing snapshots instead of writing them.\n\n## Gotchas\n- Shared state between tests causes flaky order-dependent failures. Reset in `beforeEach`.\n- \"Cannot use import statement outside a module\" means an ESM file is not transformed. Check `transform` and `transformIgnorePatterns`.\n- Path aliases from tsconfig need a matching `moduleNameMapper`.\n- Tests should never call real APIs or use real secrets, tokens or passwords. Load fake values from a test `.env` file.\n- Open handles (servers, DB pools) keep Jest running. Close them in `afterAll`, and use `--detectOpenHandles` to find them.\n\n## Portability\nThe 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.\n\n## Example prompt\n\"Add Jest tests for src/billing/applyCoupon.ts. Mock the coupon API, cover expired and invalid codes, and make sure mocks reset between tests.\"\n\n## Done when\n- New tests pass locally and with `jest --ci`.\n- Each test checks one behaviour and passes when run on its own.\n- Mocks are reset between tests and no test touches the network.\n- Coverage thresholds are set and enforced in CI.\n\n---\nAdapted from \"javascript-typescript-jest\" in github/awesome-copilot (MIT, Copyright GitHub, Inc.). See LICENSE.txt.","files":["LICENSE.txt"],"license":"MIT","license_name":"MIT License","license_url":"https://opensource.org/license/mit","attribution":"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","install":{"claude_code":"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":"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'","agents":"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'","mcp_url":"https://theskillvault.ai/mcp","cursor_mcp_deeplink":"cursor://anysphere.cursor-deeplink/mcp/install?name=theskillvault&config=eyJ1cmwiOiJodHRwczovL3RoZXNraWxsdmF1bHQuYWkvbWNwIn0%3D","mcp_config":{"mcpServers":{"theskillvault":{"url":"https://theskillvault.ai/mcp"}}}},"bundles":[],"tested_on":[{"family":"chatgpt","name":"ChatGPT","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"claude","name":"Claude","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"gemini","name":"Gemini","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"grok","name":"Grok","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"llama","name":"Llama","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"deepseek","name":"DeepSeek","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"mistral","name":"Mistral","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"qwen","name":"Qwen","outcome":null,"date":null,"source":null,"counts":false,"stale":false},{"family":"kimi","name":"Kimi K","outcome":null,"date":null,"source":null,"counts":false,"stale":false}],"proven_rule":{"threshold":5,"window_days":365,"computed_at":"2026-11-03T21:40:06.669Z"}}