Skip to content
Jest Testing Deep Dive — Matchers, Mocks, Coverage & ESM Support

Jest Testing Deep Dive — Matchers, Mocks, Coverage & ESM Support

DodaTech Updated Jun 20, 2026 8 min read

Jest is a complete JavaScript testing framework with built-in mocking, code coverage, and assertions. Going beyond the basics unlocks powerful testing patterns.

What You’ll Learn

In this tutorial, you’ll learn advanced Jest techniques: all matcher types including custom matchers with expect.extend, mocking with jest.mock, jest.fn, and jest.spyOn, fake timers for time-dependent code, snapshot testing strategies, code coverage configuration, watch mode and custom reporters, transformers for TypeScript (ts-jest) and ESM support with moduleNameMapper.

Why It Matters

Good tests catch regressions and document behaviour. Jest’s advanced features let you test async code, mock external dependencies, verify UI output with snapshots, and integrate seamlessly with modern JavaScript and TypeScript projects.

Real-World Use

When a payment processing function calls a third-party API, Jest mocks that API to return controlled responses. When testing a date formatter, fake timers freeze time to produce deterministic results. When a React component renders a list, snapshot testing catches unintended UI changes. Durga Antivirus Pro uses Jest to test its JavaScript-based threat analysis engine.

    graph TD
  A[Test Suite] --> B{Test Type}
  B --> C[Unit Tests]
  B --> D[Integration Tests]
  B --> E[Snapshot Tests]
  C --> F[Matchers]
  C --> G[Mocks]
  C --> H[Fake Timers]
  F --> I[expect().toEqual()]
  F --> J[expect().toMatchObject()]
  F --> K[Custom Matchers]
  G --> L[jest.fn]
  G --> M[jest.spyOn]
  G --> N[jest.mock]
  H --> O[jest.useFakeTimers]
  E --> P[toMatchSnapshot]
  E --> Q[toMatchInlineSnapshot]
  

Matchers In Depth

Common Matchers

// Primitives
expect(42).toBe(42);
expect('hello').toBe('hello');
expect(true).toBe(true);

// Object equality (deep compare)
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });

// Partial match
const user = { id: 1, name: 'Alice', role: 'admin' };
expect(user).toMatchObject({ name: 'Alice' });

// Array containment
expect([1, 2, 3]).toContain(2);
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });

// Type checking
expect('hello').toEqual(expect.any(String));
expect(42).toEqual(expect.any(Number));
expect({}).toEqual(expect.any(Object));

// Null/undefined
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(undefined).not.toBeDefined();

Async Matchers

// Promises
await expect(Promise.resolve('ok')).resolves.toBe('ok');
await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');

// Async/await
test('async test', async () => {
  const result = await fetchData();
  expect(result).toEqual({ id: 1 });
});

// Callbacks with done
test('callback test', (done) => {
  fetchData((error, data) => {
    expect(error).toBeNull();
    expect(data).toBeDefined();
    done();
  });
});

Custom Matchers with expect.extend

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} to be within range ${floor}${ceiling}`,
    };
  },

  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);
    return {
      pass,
      message: () => `expected ${received} to be a valid email`,
    };
  },
});

test('custom matchers', () => {
  expect(50).toBeWithinRange(0, 100);
  expect(150).not.toBeWithinRange(0, 100);
  expect('user@example.com').toBeValidEmail();
  expect('invalid').not.toBeValidEmail();
});

Mocking: jest.fn, jest.spyOn, jest.mock

jest.fn — Function Mocks

const mockFn = jest.fn();

mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'success' });
mockFn.mockRejectedValue(new Error('fail'));
mockFn.mockImplementation((x) => x * 2);
mockFn.mockImplementationOnce((x) => x);  // Single-use

// Usage
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenCalledTimes(1);

jest.spyOn — Spy on Existing Objects

const math = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};

test('spy on methods', () => {
  const addSpy = jest.spyOn(math, 'add');

  const result = math.add(2, 3);
  expect(result).toBe(5);
  expect(addSpy).toHaveBeenCalledWith(2, 3);

  addSpy.mockRestore();  // Restore original
});

test('spy on console', () => {
  const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

  console.warn('Something is wrong');
  expect(warnSpy).toHaveBeenCalledWith('Something is wrong');

  warnSpy.mockRestore();
});

jest.mock — Module-Level Mocking

// api.js
export async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// __tests__/api.test.js
jest.mock('../api');

import { fetchUser } from '../api';

test('fetchUser returns mocked data', async () => {
  fetchUser.mockResolvedValue({ id: 1, name: 'Alice' });

  const user = await fetchUser(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
  expect(fetchUser).toHaveBeenCalledWith(1);
});

Manual Mocks with mocks

Create __mocks__/fs.js in your project:

const fs = jest.createMockFromModule('fs');

const mockFiles = new Map();

fs.readFileSync = jest.fn((path) => {
  if (mockFiles.has(path)) {
    return mockFiles.get(path);
  }
  throw new Error(`File not found: ${path}`);
});

fs.writeFileSync = jest.fn((path, data) => {
  mockFiles.set(path, data);
});

module.exports = fs;

Fake Timers

Time-dependent code is hard to test without fake timers. Jest can mock setTimeout, setInterval, Date.now, and more.

jest.useFakeTimers();

function scheduleNotification(message, delayMs) {
  setTimeout(() => {
    console.log(`Notification: ${message}`);
  }, delayMs);
}

test('scheduleNotification uses setTimeout', () => {
  const spy = jest.spyOn(global, 'setTimeout');

  scheduleNotification('Hello!', 5000);
  expect(spy).toHaveBeenCalledWith(expect.any(Function), 5000);
});

test('fast-forward time', () => {
  const callback = jest.fn();

  setTimeout(callback, 10000);
  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersByTime(10000);
  expect(callback).toHaveBeenCalledTimes(1);
});

test('run all pending timers', () => {
  const callback = jest.fn();
  setInterval(callback, 1000);

  jest.runAllTimers();
  expect(callback).toHaveBeenCalled();
});

Snapshot Testing

Snapshots capture the rendered output of a component and compare against previous runs.

// Component
function UserProfile({ user }) {
  return `<div class="profile">
    <h2>${user.name}</h2>
    <p>${user.email}</p>
  </div>`;
}

// Test
test('UserProfile matches snapshot', () => {
  const user = { name: 'Alice', email: 'alice@example.com' };
  expect(UserProfile({ user })).toMatchSnapshot();
});

// Inline snapshot (stores in the test file)
test('inline snapshot', () => {
  const user = { name: 'Bob', email: 'bob@test.com' };
  expect(UserProfile({ user })).toMatchInlineSnapshot();
});

When snapshots fail: review the diff. If the change is intentional, update with jest --updateSnapshot or press u in watch mode.

Code Coverage

# Generate coverage report
npx jest --coverage

# With thresholds
npx jest --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80,"statements":80}}'

Configuration in jest.config.js:

module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,ts,jsx,tsx}',
    '!src/**/*.d.ts',
    '!src/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 85,
      lines: 85,
      statements: 85,
    },
  },
  coverageReporters: ['text', 'lcov', 'html'],
  coverageDirectory: 'coverage',
};

Watch Mode

# Default watch mode
npx jest --watch

# Watch all files
npx jest --watchAll

Watch mode offers hotkeys:

  • a — run all tests
  • f — run only failed tests
  • o — run only tests related to changed files
  • p — filter by test filename pattern
  • t — filter by test name pattern
  • q — quit

Custom Reporters

// custom-reporter.js
class CustomReporter {
  constructor(globalConfig, options) {
    this._globalConfig = globalConfig;
    this._options = options;
  }

  onRunStart(results) {
    console.log('Test suite started');
  }

  onRunComplete(testContexts, results) {
    const { numPassedTests, numFailedTests } = results;
    console.log(`Passed: ${numPassedTests}, Failed: ${numFailedTests}`);
  }

  onTestResult(test, testResult, aggregatedResult) {
    console.log(`  ${testResult.testFilePath}: ${testResult.numFailingTests > 0 ? 'FAIL' : 'PASS'}`);
  }
}

module.exports = CustomReporter;

Usage:

npx jest --reporters=default --reporters=./custom-reporter.js

Transformers

babel-jest (default for JS)

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
    ['@babel/preset-react', { runtime: 'automatic' }],
  ],
};

ts-jest (TypeScript without Babel)

// jest.config.js
module.exports = {
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testEnvironment: 'node',
};

ESM Support for ts-jest

// jest.config.js for ESM
module.exports = {
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      { useESM: true },
    ],
  },
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
};

moduleNameMapper

Maps module paths for Jest, useful for path aliases and CSS modules.

module.exports = {
  moduleNameMapper: {
    // Path aliases (like tsconfig paths)
    '^@/(.*)$': '<rootDir>/src/$1',

    // CSS modules — mock as empty objects
    '\\.(css|less|scss)$': '<rootDir>/__mocks__/styleMock.js',

    // Images — mock as path string
    '\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js',

    // Plain text
    '\\.(txt|md)$': '<rootDir>/__mocks__/textMock.js',
  },
};

Common Mistakes

1. Using toBe for objects

toBe uses Object.is — it checks reference equality. Use toEqual or toStrictEqual for deep object comparison.

2. Not resetting mocks between tests

Mock state leaks between tests. Use beforeEach(() => { jest.clearAllMocks(); }) or set clearMocks: true in jest config.

3. Testing implementation details

Testing private methods or internal state makes tests brittle. Test the public API and observable behaviour instead.

4. Over-mocking

Mocking everything hides integration bugs. Use a mix of unit tests (mocked dependencies) and integration tests (real dependencies).

5. Ignoring snapshot diffs

Accepting snapshot updates without reviewing the diff defeats their purpose. Always inspect which lines changed before pressing u.

6. Not handling ESM correctly

Jest needs special config for ESM modules. Use transform with ts-jest or babel-jest, and configure moduleNameMapper for .js extensions.

Practice Questions

  1. What’s the difference between toBe and toEqual? toBe checks reference equality (Object.is). toEqual performs deep equality comparison of values and structures.

  2. How do you mock a module in Jest? Use jest.mock('./module') at the top of the test file. Jest automatically replaces module exports with mock functions.

  3. What does jest.useFakeTimers() do? It replaces setTimeout, setInterval, Date.now, and other timing functions with mock implementations you control.

  4. How do you update snapshots? Run jest --updateSnapshot or press u in interactive watch mode after verifying the changes are correct.

  5. What is moduleNameMapper used for? It maps import paths to mock modules at test time — useful for path aliases, CSS modules, and asset files.

Challenge

Write a test suite for a rate-limiter function that allows 5 calls per minute. Use fake timers to verify behaviour:

  • Calls 1-5 succeed
  • Call 6 within the same minute fails
  • After 60 seconds, calls succeed again

Real-World Task

Take an existing module in your project and add tests using:

  • jest.mock for external dependencies
  • Custom matcher for domain-specific assertions
  • Coverage thresholds to ensure at least 80% coverage

FAQ

What is the difference between jest.fn() and jest.spyOn()?
jest.fn creates a new mock function. jest.spyOn wraps an existing method on an object, letting you call the original or replace it with a mock.
How do I test code that uses fetch?
Mock the global fetch or use jest.mock on your API module. For more realistic tests, use msw (Mock Service Worker) to intercept network requests.
What is toStrictEqual vs toEqual?
toStrictEqual is stricter — it checks that objects have the same types and no undefined properties. toEqual ignores undefined properties and coerces types in some cases.
How do I run a single test?
Use it.only('test name', ...) or describe.only(...). From the CLI: jest --testNamePattern="pattern" or jest path/to/test.
Can Jest test TypeScript?
Yes. Use ts-jest transformer or babel-jest with @babel/preset-typescript. Both compile TypeScript to JavaScript before running tests.

Mini Project: Test Infrastructure

Build a test infrastructure for a small utility library:

  1. Module mocking for external API calls
  2. Custom matchers for business logic
  3. Snapshot tests for output formatting
  4. Coverage thresholds at 90%+
  5. CI workflow that runs tests and reports coverage

What’s Next

Before moving on, you should understand:

  • All Jest matcher types including custom matchers
  • Mocking strategies (jest.fn, jest.spyOn, jest.mock)
  • Fake timers and snapshot testing
  • Code coverage configuration and thresholds
  • Transformer config for TypeScript and ESM

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro