Jest Framework: A Practical Guide (Mocks, spyOn, Coverage, CI) — 2026 Edition
The Jest framework is still the go-to testing setup for a ton of JavaScript and TypeScript teams in 2026 because it’s productive out of the box: a test runner, assertions, mocking, snapshots, and coverage in one place.
This Jest tutorial is written for busy engineers. You’ll learn:
the 80/20 of writing great tests (AAA pattern)
mocks that don’t rot
Jest spyOn (and the common “why is this not spying?!” traps)
coverage that’s not fake confidence
CI that stays stable
Table of contents
What is the Jest framework?
Install Jest (JS/TS) fast
Jest tutorial: the AAA pattern that keeps tests readable
Mocks in Jest:
jest.fnvsjest.mockvsjest.spyOnJest spyOn explained (real examples + pitfalls)
Coverage in Jest (thresholds + providers)
CI setup (GitHub Actions) + common flake fixes
FAQ (targets “jest tutorial” + “jest spyon”)

Get the Mobile Testing Playbook Used by 800+ QA Teams
Discover 50+ battle-tested strategies to catch critical bugs before production and ship 5-star apps faster.
1) What is the Jest framework?
Jest is a JavaScript testing framework commonly used for unit tests and component tests in JS/TS projects. It also provides mocking utilities and built-in coverage reporting.
If you’re modernizing your stack: Jest 30 requires at least Node 18.x (and has TypeScript minimum changes), so keep your CI/runtime aligned.
2) Install Jest (JS + TypeScript) in minutes
JavaScript (Node) setup
npm i -D jest
TypeScript setup (practical default)
npm i -D jest ts-jest @types/jest
Add scripts:
{"scripts": {"test": "jest","test:watch": "jest --watch","test:ci": "jest --ci"}}
Basic config (jest.config.js):
/** @type {import('jest').Config} */module.exports = {testEnvironment: "node",clearMocks: true,testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],};
If you’re on Next.js, use their official Jest guide because config differs (App Router vs Pages Router, CSS modules, etc.).
3) Jest tutorial: write tests that don’t suck (AAA pattern)
Most Jest test suites become painful for one reason: tests are hard to read. Fix that with AAA:
Arrange: inputs + mocks
Act: execute the unit
Assert: verify outcome + side effects
Example:
import { calculateTotal } from "./billing";test("adds tax to subtotal", () => {// Arrangeconst subtotal = 100;const taxRate = 0.18;// Actconst total = calculateTotal(subtotal, taxRate);// Assertexpect(total).toBe(118);});
Want a strict template with naming rules? Check aaa pattern of unit testing out.
4) Mocks in the Jest framework: jest.fn vs jest.mock vs jest.spyOn
Here’s the simplest mental model (and yes, this is what keeps suites maintainable):
jest.fn() — mock a function you control
Use when you own the function reference and just need call tracking + return values.
const sendEmail = jest.fn().mockResolvedValue({ ok: true });await sendEmail("a@b.com");expect(sendEmail).toHaveBeenCalledWith("a@b.com");
Jest’s docs call these “mock functions” and they can act as spies because they let you observe calls.
jest.mock() — mock an entire module (powerful, easy to overuse)
Use when you need to isolate external boundaries (payments, analytics SDKs, filesystem, etc.).
jest.mock("./payments", () => ({chargeCard: jest.fn().mockResolvedValue({ id: "ch_123" }),}));
Rule: mock boundaries, not internals. Module mocks are how teams accidentally create “tests that pass while prod breaks.”
jest.spyOn() — spy on a real method (best for “verify it was called”)
Use when you want to observe calls on an existing object method, and optionally override it.
And yes—people often type “jest spyon” in Google when they mean Jest spyOn. This section is for that exact search intent.
5) Jest spyOn explained (with real examples + common pitfalls)
Example A: spy without changing behavior
import { analytics } from "./analytics";test("tracks purchase event", () => {const spy = jest.spyOn(analytics, "trackEvent");analytics.trackEvent("purchase", { price: 299 });expect(spy).toHaveBeenCalledWith("purchase", { price: 299 });});
Example B: spy + override behavior (common in unit tests)
const spy = jest.spyOn(apiClient, "getUser").mockResolvedValue({ id: 7, name: "Asha" });const result = await handler(7);expect(spy).toHaveBeenCalledWith(7);expect(result.name).toBe("Asha");
Pitfall #1 (most common): you spied on the wrong reference
If production code imports a function directly:
import { trackEvent } from "./analytics";
…and your test spies on an object:
jest.spyOn(analytics, "trackEvent");
…your spy might never see calls because the runtime uses the imported binding.
Fix: spy on the same reference the code uses (often by importing the whole module):
import * as analyticsModule from "./analytics";test("tracks purchase", () => {const spy = jest.spyOn(analyticsModule, "trackEvent");analyticsModule.trackEvent("purchase", { price: 299 });expect(spy).toHaveBeenCalled();});
Pitfall #2: you forgot to restore spies (leaks state across tests)
This is the #1 reason suites become flaky over time.
Add:
afterEach(() => {jest.restoreAllMocks();});
Important detail: jest.restoreAllMocks() only reliably restores mocks created with jest.spyOn() (and some replaced properties).
Pitfall #3: using mockClear when you needed mockRestore
Quick cheat sheet:
mockClear()= clears call historymockReset()= clears call history + resets implementationmockRestore()= restores original implementation (spyOn only)
6) Coverage in Jest: don’t let the number lie to you
In the Jest framework, coverage is easy:
jest --coverage
But coverage can still mislead you if config is sloppy or CI uploads partial results.
Set thresholds (so CI fails when quality slips)
In jest.config.js:
module.exports = {collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}","!src/**/*.d.ts",],coverageThreshold: {global: {branches: 80,functions: 80,lines: 80,statements: 80,},},};
Coverage provider: babel vs v8
Jest supports a coverageProvider option with allowed values babel (default) or v8.
Practical rule:
If sourcemaps/transforms are confusing your reports, try
coverageProvider: "v8"in CI and validate on a few PRs.
7) CI setup: GitHub Actions that stays stable
A clean job:
name: test on: [push, pull_request]
jobs:jest:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 20- run: npm ci- run: npm test -- --ci --coverage
If you’re on Jest 30+, ensure Node is >= 18.x, otherwise CI will break in annoying ways.
Next.js note (real-world gotcha)
Next’s official docs call out current limitations around testing async Server Components with Jest, and often recommend E2E for those paths.
8) Best practices that keep Jest suites healthy
Prefer testing behavior over implementation: If every test is
spyOn+ “called with X”, you’re testing wiring, not correctness.Mock boundaries, not your own core logic: Mock network, DB clients, analytics, payment SDKs—avoid mocking your domain functions unless you have a specific reason.
Use AAA + consistent naming: Your future self will thank you. Link for AAA Pattern in Unit Testing
Coverage is a map, not proof: A file can be “covered” with zero meaningful assertions.
Pair unit/component tests with real execution for UX-critical flows: Unit tests won’t catch “checkout broke on a real device because UI timing changed.” That’s where device-level execution testing matters.
FAQ (targets “jest tutorial” + “jest spyon” search intent)
What is the Jest framework used for?
Unit tests and component tests in JS/TS projects, with built-in mocking utilities and coverage tooling.
What is “jest spyon”?
It’s a common misspelling of Jest spyOn (jest.spyOn()), used to observe calls to an existing method and optionally override it.
Does jest.restoreAllMocks() restore everything?
No. Jest warns it mainly works for mocks created with jest.spyOn() (and certain replaced properties).
Is Jest 30 safe to upgrade to?
Yes, but verify your Node version (>= 18.x) and follow upgrade notes, especially in CI.




