Jest Testing Deep Dive — Matchers, Mocks, Coverage & ESM Support
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 --watchAllWatch mode offers hotkeys:
a— run all testsf— run only failed testso— run only tests related to changed filesp— filter by test filename patternt— filter by test name patternq— 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.jsTransformers
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
What’s the difference between
toBeandtoEqual?toBechecks reference equality (Object.is).toEqualperforms deep equality comparison of values and structures.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.What does
jest.useFakeTimers()do? It replacessetTimeout,setInterval,Date.now, and other timing functions with mock implementations you control.How do you update snapshots? Run
jest --updateSnapshotor pressuin interactive watch mode after verifying the changes are correct.What is
moduleNameMapperused 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.mockfor external dependencies- Custom matcher for domain-specific assertions
- Coverage thresholds to ensure at least 80% coverage
FAQ
Mini Project: Test Infrastructure
Build a test infrastructure for a small utility library:
- Module mocking for external API calls
- Custom matchers for business logic
- Snapshot tests for output formatting
- Coverage thresholds at 90%+
- 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