Jest Testing Guide | Unit Tests, Mocks, Snapshots, Coverage & React

Jest Testing Guide | Unit Tests, Mocks, Snapshots, Coverage & React

이 글의 핵심

This guide walks through Jest from first test to coverage thresholds and React component tests. It also explains how the runner schedules work, how module mocking is hoisted, how Istanbul instruments code for coverage, and how teams combine unit tests with staging E2E checks before production.

Introduction

Jest is a batteries-included test runner for JavaScript and TypeScript. This article mirrors the practical flow of the Korean jest-testing-guide post—installation, matchers, async tests, mocks, React Testing Library, snapshots, and coverage—then adds internals so you can debug odd failures and design a test strategy that survives CI and release pipelines.

For a longer reference with more patterns, see the Jest Complete Guide.


Test runner architecture (how Jest executes your suite)

At a high level, Jest:

  1. Discovers test files using testMatch / testRegex and optional projects.
  2. Builds a dependency graph (via jest-haste-map) so it knows which files to transform and cache.
  3. Spawns worker processes (jest-worker) to run test files in parallel (CPU-bound; I/O-bound work still benefits from parallelism up to a point).
  4. Applies transforms (Babel, ts-jest, or babel-jest) per file, then loads each test file in an isolated VM context (or jest-environment-jsdom / node).

Implications:

  • Flaky order: Tests in different files run in parallel; tests in one file run serially by default. Shared global state breaks under parallel workers—reset mocks and avoid mutating process.env without setupFiles discipline.
  • Slow cold start: Large monorepos pay for haste-map and transform caches. Use --maxWorkers in CI to cap memory; use cacheDirectory intentionally on CI caches.
  • --watch: Uses file watchers to re-run only affected tests when the graph is known.

Mocking mechanisms

jest.fn() — call tracking

jest.fn() creates a function that records every invocation, arguments, return values, and thrown errors. Use it to stub callbacks and verify collaboration between units.

jest.mock('module') — module substitution

jest.mock replaces a module with an auto-mocked version or a factory you provide. Jest hoists these calls: the mock is registered before the rest of the module executes, which surprises newcomers when imports already resolved.

Patterns:

  • Partial mock: spread jest.requireActual inside the factory and override one export.
  • Manual mocks: place __mocks__ adjacent to node_modules or next to the module for predictable replacements in large codebases.

jest.spyOn — observe without replacing

jest.spyOn(obj, 'method') wraps a real method: you can assert calls and optionally mockImplementation or mockRestore() to return to the original.

Timers and environment

jest.useFakeTimers() replaces setTimeout / Date with simulated time—essential for debounce logic. Pair with jest.advanceTimersByTime and remember to jest.useRealTimers() in afterEach when tests mix real and fake time.


Coverage instrumentation

When you run jest --coverage, Jest uses Istanbul (via babel-plugin-istanbul or equivalent) to insert counters into branches, functions, lines, and statements. The report answers “what ran?”—not “what is correct?”.

Configure meaningful gates:

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{js,ts,tsx}',
    '!src/**/*.test.{js,ts,tsx}',
    '!src/index.tsx',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Exclude generated files, barrel index files that only re-export, and pure types—otherwise thresholds become theater.


Production-oriented testing patterns

Jest itself does not run inside your production servers for user traffic. “Production testing” usually means:

  • CI on every merge: jest --ci --coverage --maxWorkers=2 with deterministic seeds where applicable.
  • Staging E2E: Playwright or Cypress against a deployed preview—user journeys and cross-service contracts.
  • Synthetic monitoring: scheduled probes from outside (ping, API checks, browser scripts)—orthogonal to Jest but part of the same quality story.
  • Feature flags + canary: route a slice of traffic after unit/integration/E2E gates pass.

Treat unit tests as fast feedback on pure logic, integration tests on module boundaries (DB, HTTP with test containers), and E2E as the last line before customers—each layer catches different failure modes.


Install and minimal config

npm install -D jest @types/jest
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: ['src/**/*.{js,ts}', '!src/**/*.test.{js,ts}'],
};

Matchers, async tests, mocks, RTL, snapshots

The Korean companion post walks through matchers (toEqual, toThrow), async/await tests, jest.mock for modules, React Testing Library examples, and toMatchSnapshot() for components. The patterns are identical in English codebases—prefer roles and labels from Testing Library over CSS selectors.


Checklist

  • Install Jest and a transform (ts-jest or Babel) for TypeScript if needed.
  • Stabilize mocks (clearMocks / resetMocks) and avoid shared mutable singletons across files.
  • Set realistic coverage thresholds on src only.
  • Wire test:ci into GitHub Actions (or your CI) with caching for node_modules and Jest cache.