From jest-consistency
Write, review, refactor, or debug Jest tests (jest.mock, jest.fn, spyOn, fake timers, matchers, snapshots) using one canonical, modern idiom set. Use this skill whenever code creates or fixes unit tests with Jest, mocks modules or timers, tests async functions and rejections, or when the user hits "The module factory of jest.mock() is not allowed to reference any out-of-scope variables", mocks bleeding between tests, tests that pass but shouldn't (missing await), "Jest did not exit one second after the test run", or asks toBe vs toEqual. Trigger it even when the user just says "add tests for this module" in a Jest project — without saying the words "Jest idioms."
How this skill is triggered — by the user, by Claude, or both
Slash command
/jest-consistency:jest-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Jest is stable and ubiquitous in training data — which means generated tests reproduce the
Jest is stable and ubiquitous in training data — which means generated tests reproduce the ecosystem's classic mistakes: hoisting-violating mock factories, mock state bleeding across tests, unawaited async assertions that pass vacuously, and matchers chosen by habit rather than semantics. This skill pins one canonical idiom set — Jest 29 semantics — built around mock hygiene and honest async tests.
| Always | Never | Why |
|---|---|---|
jest.mock("./mod") at top level, config in the test via jest.mocked(mod).fn.mockReturnValue(...) | a jest.mock factory referencing variables declared next to it | jest.mock is hoisted above imports; out-of-scope captures throw (only mock*-prefixed vars are exempt). |
clearMocks: true in config (or beforeEach(() => jest.clearAllMocks())) | trusting test order to leave mocks clean | Call counts/results otherwise leak between tests — false greens and order-dependent failures. |
jest.spyOn(obj, "m").mockImplementation(...) + afterEach(() => jest.restoreAllMocks()) | obj.m = jest.fn() reassignment | spyOn is restorable; manual reassignment permanently mutates the module for later tests. |
await expect(fn()).rejects.toThrow("boom") | try { await fn(); } catch (e) { expect(e)... } | The try/catch form passes silently when nothing throws (unless you add expect.assertions). |
it("...", async () => { await ... }) — await every assertion-bearing promise | mixing done callbacks with promises / forgetting await | An unawaited rejection escapes the test: it passes now, fails the run later (or never). |
toStrictEqual for object equality (house default) | reflexive toEqual everywhere | toEqual ignores undefined props and class identity; toStrictEqual doesn't. toBe only for primitives/identity. |
jest.useFakeTimers() + jest.advanceTimersByTime(n) / await jest.advanceTimersByTimeAsync(n), then jest.useRealTimers() in afterEach | real setTimeout + bigger test timeouts | Real waits are slow and flaky; unrestored fake timers break later tests and libraries. |
test.each([[in1, out1], [in2, out2]])("f(%s)", ...) | copy-pasted near-identical tests or a for loop inside one test | A loop is one test that stops at the first failure; each reports each case. |
small toMatchInlineSnapshot() with intent | sprawling file snapshots of whole component trees | Big snapshots get blindly -u-updated; they assert nothing. |
House style for a mocked async test:
import { fetchUser } from "./api";
import { getUserName } from "./user";
jest.mock("./api");
const mockFetchUser = jest.mocked(fetchUser);
describe("getUserName", () => {
it("returns the fetched name", async () => {
mockFetchUser.mockResolvedValue({ id: 1, name: "Ada" });
await expect(getUserName(1)).resolves.toBe("Ada");
expect(mockFetchUser).toHaveBeenCalledWith(1);
});
it("propagates API failures", async () => {
mockFetchUser.mockRejectedValue(new Error("503"));
await expect(getUserName(1)).rejects.toThrow("503");
});
});
await before expect(p).resolves/rejects... makes the assertion float; the
test ends first and passes regardless. These two matchers must always be awaited.expect.assertions(1) if you must use
try/catch; prefer rejects/toThrow forms.expect(fn(badInput)).toThrow() — wrong: the call throws before expect runs. Pass
a function: expect(() => fn(badInput)).toThrow().clearAllMocks zeroes calls/results;
resetAllMocks also removes implementations (mockReturnValue is gone!);
restoreAllMocks only restores spyOn originals. Choosing reset when you meant clear
silently strips configured return values mid-suite.jest.isolateModules/jest.resetModules when testing module init.setTimeout wrapped in a promise needs
advanceTimersByTimeAsync (or interleaved microtask flushes); plain advanceTimersByTime
leaves the promise pending.--forceExit.Target Jest 29. Notables on the way here: jest.mocked built in (27+), fake timers
modern by default (27), advanceTimersByTimeAsync (29), snapshot format changes (29).
ESM support is still experimental — under real ESM, prefer dependency injection or
jest.unstable_mockModule; flag it rather than pretending jest.mock works the same.
jest.mock (whole module), or spyOn
(one method, restorable). Mock at the boundary you own.clearMocks: true for hygiene.await all resolves/rejects, return nothing else.toBe identity, toStrictEqual structure,
toMatchObject partial, toThrow with a message/class.done callbacks in promise code.For the fuller mock-type reference, timer recipes, async matrices, and config defaults,
read references/jest-patterns.md.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub guidogl/jest-consistency --plugin jest-consistency