Jest is a delightful JavaScript testing framework developed and maintained by Meta (formerly Facebook). It's designed to provide a complete and developer-friendly testing experience out of the box, with zero configuration required for most JavaScript projects. Jest has become the de facto standard for testing React applications but works beautifully with any JavaScript codebase, including Node.js backends, Vue, Angular, TypeScript projects, and more.
What Is Jest?
Jest is an open-source testing framework that bundles everything you need into a single package: a test runner, an assertion library, mocking capabilities, snapshot testing, and code coverage reporting. Unlike older testing setups that require stitching together Mocha, Chai, Sinon, and Istanbul, Jest provides all these features integrated and ready to use immediately after installation.
Key features of Jest include:
Zero-configuration – Works instantly with projects using Babel, TypeScript, Node.js, React, Angular, Vue, and more
Snapshot testing – Captures and compares rendered output over time to detect unintended changes
Built-in mocking – Powerful module mocking, function mocking, and timer mocking without external libraries
Interactive watch mode – Intelligent file-watching that only runs tests related to changed files
Code coverage – Built-in Istanbul-based coverage reports with configurable thresholds
Parallel test execution – Runs tests in parallel across available CPU cores for maximum speed
Isolated test environment – Each test file runs in its own sandboxed environment to prevent cross-test contamination
Why Jest Matters
Testing is a critical part of professional software development. Without tests, refactoring becomes dangerous, bug fixes become guesswork, and onboarding new developers becomes a nightmare. Jest matters because it dramatically lowers the barrier to writing high-quality tests. Its intuitive API, fast execution, and comprehensive feature set mean developers actually enjoy writing tests rather than dreading them.
When you adopt Jest, you gain:
Confidence – Know immediately when something breaks, before it reaches production
Documentation – Tests serve as executable documentation that never goes out of date
Velocity – Safe refactoring means you can move fast without breaking things
Code quality – Writing testable code naturally leads to better architecture and cleaner interfaces
Team alignment – A shared testing culture reduces misunderstandings about expected behavior
Getting Started with Jest
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
For most projects, installing Jest is a single command. Here's how to set it up across different project types:
# For a Node.js or general JavaScript project
npm install --save-dev jest
# For a React project (using Create React App, Jest is pre-installed)
# No additional installation needed — CRA ships with Jest configured
# For TypeScript projects
npm install --save-dev jest typescript ts-jest @types/jest
function sum(a, b) {
return a + b;
}
module.exports = sum;
Now create a test file sum.test.js:
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
Run npm test and you'll see Jest's output indicating a passing test. Congratulations — you've written your first Jest test!
Core Testing Concepts
Test Blocks: test() and it()
Jest provides two equivalent functions for defining individual test cases: test() and it(). They are aliases of each other — use whichever reads more naturally in your test descriptions. Both accept a string description and a callback function containing your assertions.
// These two are functionally identical
test('returns the correct greeting', () => {
expect(greet('World')).toBe('Hello, World!');
});
it('should handle empty input gracefully', () => {
expect(greet('')).toBe('Hello, !');
});
Use it() when your description reads well as a sentence starting with "it should..." — this creates more natural language in test output. Use test() for all other cases.
Grouping Tests with describe()
The describe() block groups related tests together, creating a logical hierarchy in your test suite. You can nest describe blocks to create sub-groups, and each describe block can have its own setup and teardown hooks.
describe('sum function', () => {
// Basic arithmetic tests
describe('with positive numbers', () => {
test('adds 1 and 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('adds large numbers correctly', () => {
expect(sum(1000, 2000)).toBe(3000);
});
});
// Edge cases
describe('with zero', () => {
test('adding zero does not change the value', () => {
expect(sum(5, 0)).toBe(5);
expect(sum(0, 5)).toBe(5);
});
});
// Error conditions
describe('with invalid input', () => {
test('throws TypeError when arguments are missing', () => {
expect(() => sum()).toThrow(TypeError);
});
});
});
Assertions and Matchers
Jest's assertion library is built around the expect() function, which receives a value and returns an object with matcher methods. Matchers compare the received value against expected values and throw descriptive errors when they don't match.
Equality matchers are the most commonly used:
// Strict equality (===) for primitive values
expect(42).toBe(42);
expect('hello').toBe('hello');
// Deep equality for objects and arrays
expect({name: 'Alice', age: 30}).toEqual({name: 'Alice', age: 30});
expect([1, 2, 3]).toEqual([1, 2, 3]);
// Check for the opposite using .not
expect(10).not.toBe(5);
expect({a: 1}).not.toEqual({a: 2});
Truthiness matchers help you test for null, undefined, and boolean conditions:
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('defined value').toBeDefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy(); // 0 is falsy in JavaScript
expect('').toBeFalsy(); // empty string is falsy
Number matchers give you precise control over numeric comparisons:
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);
// For floating point, use toBeCloseTo to avoid precision issues
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // second argument is decimal precision
String matchers check string content:
expect('hello world').toMatch(/world/); // regex match
expect('hello world').toMatch('world'); // substring match
expect('hello').toHaveLength(5);
expect('HELLO').toEqual(expect.stringContaining('ELL'));
Exception matchers verify that functions throw errors:
function throwError() {
throw new Error('Something went wrong');
}
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow('Something went wrong');
expect(() => throwError()).toThrow(/went wrong/);
expect(() => throwError()).toThrow(Error); // check error type
Setup and Teardown
When tests share common preparation or cleanup logic, Jest provides lifecycle hooks that run before and after tests within a describe block.
beforeEach and afterEach
These hooks run before and after every single test in their describe block. They're perfect for resetting state, clearing mocks, or setting up a fresh testing environment for each test case.
describe('User database operations', () => {
let db;
beforeEach(() => {
// Create a fresh database connection before each test
db = createTestDatabase();
db.connect();
db.seed({users: []}); // start with empty users table
});
afterEach(() => {
// Clean up after each test
db.disconnect();
db.destroy();
});
test('inserts a new user into the database', () => {
const user = {name: 'Bob', email: 'bob@test.com'};
db.users.insert(user);
expect(db.users.count()).toBe(1);
});
test('retrieves a user by email', () => {
db.users.insert({name: 'Carol', email: 'carol@test.com'});
const found = db.users.findByEmail('carol@test.com');
expect(found.name).toBe('Carol');
});
});
beforeAll and afterAll
These hooks run once before all tests in the describe block and once after all tests complete. They're ideal for expensive operations like starting a server, establishing a database connection, or compiling assets that can be shared across multiple tests.
describe('API integration tests', () => {
let server;
beforeAll(async () => {
// Start the server once — this is expensive
server = await startTestServer({port: 9999});
console.log('Test server started on port 9999');
});
afterAll(async () => {
// Shut down the server once all tests are done
await server.stop();
console.log('Test server stopped');
});
test('GET /api/users returns 200', async () => {
const response = await fetch('http://localhost:9999/api/users');
expect(response.status).toBe(200);
});
test('POST /api/users creates a user', async () => {
const response = await fetch('http://localhost:9999/api/users', {
method: 'POST',
body: JSON.stringify({name: 'Dave'}),
headers: {'Content-Type': 'application/json'}
});
expect(response.status).toBe(201);
});
});
Scoping Setup and Teardown
Lifecycle hooks are scoped to their nearest enclosing describe block. You can nest describe blocks to create different setup contexts:
describe('Authentication module', () => {
let auth;
beforeAll(() => {
auth = new AuthenticationService('test-secret-key');
});
describe('when the user is not logged in', () => {
beforeEach(() => {
auth.clearSession();
});
test('login() returns a new token', () => {
const token = auth.login('user1', 'password');
expect(token).toBeDefined();
expect(auth.isAuthenticated()).toBe(true);
});
test('getUser() throws when no session exists', () => {
expect(() => auth.getUser()).toThrow('No active session');
});
});
describe('when the user is already logged in', () => {
beforeEach(() => {
auth.login('user1', 'password');
});
test('getUser() returns the current user', () => {
const user = auth.getUser();
expect(user.username).toBe('user1');
});
test('logout() clears the session', () => {
auth.logout();
expect(auth.isAuthenticated()).toBe(false);
});
});
});
Order of Execution
Understanding execution order is crucial for avoiding subtle bugs. Jest executes hooks in this sequence:
Modern applications are full of asynchronous operations — API calls, database queries, file I/O, timers, and promises. Jest provides several patterns for testing async code, and choosing the right one prevents flaky tests and hard-to-debug timeouts.
Callbacks
For callback-based code, Jest provides a done callback. If done() is never called, the test will timeout and fail. If done() is called with an argument, Jest treats that argument as an error.
function fetchUserData(id, callback) {
setTimeout(() => {
callback(null, {id, name: 'Alice'});
}, 100);
}
test('fetchUserData returns user via callback', (done) => {
fetchUserData(1, (error, user) => {
expect(error).toBeNull();
expect(user.name).toBe('Alice');
expect(user.id).toBe(1);
done(); // Signal that the test is complete
});
});
test('handles callback errors', (done) => {
function failingOperation(cb) {
setTimeout(() => cb(new Error('Network error')), 50);
}
failingOperation((err) => {
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('Network error');
done();
});
});
Promises
For promise-based code, simply return the promise from the test function. Jest will wait for the promise to resolve before considering the test complete. If the promise rejects, the test fails automatically.
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id < 1) reject(new Error('Invalid ID'));
else resolve({id, name: 'Bob'});
}, 50);
});
}
test('fetchUser resolves with user data', () => {
// Return the promise — Jest waits for it
return fetchUser(5).then(user => {
expect(user.name).toBe('Bob');
expect(user.id).toBe(5);
});
});
test('fetchUser rejects for invalid IDs', () => {
return fetchUser(0).catch(error => {
expect(error.message).toBe('Invalid ID');
});
});
You can also use the .resolves and .rejects matchers for cleaner promise assertions:
Async/await is the most readable and maintainable pattern for testing asynchronous code. Simply declare your test function as async and use await within it.
test('fetches user with async/await', async () => {
const user = await fetchUser(10);
expect(user.id).toBe(10);
expect(user.name).toBe('Bob');
});
test('handles rejection with async/await', async () => {
await expect(fetchUser(-5)).rejects.toThrow('Invalid ID');
});
// Testing multiple sequential async operations
test('creates order and verifies it', async () => {
const order = await createOrder({item: 'Book', quantity: 2});
expect(order.status).toBe('pending');
const processed = await processOrder(order.id);
expect(processed.status).toBe('completed');
expect(processed.total).toBeGreaterThan(0);
});
Use expect.assertions() when testing async code to ensure that assertions actually run. This is especially important in .catch() blocks where a missing assertion could silently pass:
test('ensures assertions run in catch block', async () => {
expect.assertions(1); // Ensure exactly 1 assertion runs
try {
await fetchUser(-1);
// Should not reach here
} catch (error) {
expect(error.message).toBe('Invalid ID');
}
});
Timer-Based Async Code
Jest can mock timers so you don't have to wait for real setTimeout or setInterval calls. This makes timer-dependent tests fast and deterministic.
function delayedGreeting(name, delayMs) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Hello, ${name}!`);
}, delayMs);
});
}
// Enable fake timers before tests
beforeEach(() => {
jest.useFakeTimers();
});
// Restore real timers after tests
afterEach(() => {
jest.useRealTimers();
});
test('delayedGreeting resolves after specified delay', async () => {
// Don't await yet — start the promise
const promise = delayedGreeting('World', 5000);
// Fast-forward timers by 5000ms
jest.advanceTimersByTime(5000);
// Now await the resolved promise
const result = await promise;
expect(result).toBe('Hello, World!');
});
test('handles multiple timers', async () => {
const promise1 = delayedGreeting('Alice', 1000);
const promise2 = delayedGreeting('Bob', 3000);
// Advance past the first timer
jest.advanceTimersByTime(1000);
expect(await promise1).toBe('Hello, Alice!');
// Advance past the second timer
jest.advanceTimersByTime(2000);
expect(await promise2).toBe('Hello, Bob!');
});
test('runAllTimers executes all pending timers', async () => {
const promise = delayedGreeting('Charlie', 10000);
jest.runAllTimers(); // Execute all timers immediately
const result = await promise;
expect(result).toBe('Hello, Charlie!');
});
Mocking and Spying
Mocking is one of Jest's most powerful features. It allows you to replace real implementations with controlled substitutes, isolate the code under test, and verify interactions between components.
Mock Functions: jest.fn()
A mock function created with jest.fn() records every call, every argument, and every this context. You can control its return value, implementation, and inspect its call history.
test('mock function records calls and arguments', () => {
const mockCallback = jest.fn();
// Use the mock in your code
mockCallback('arg1');
mockCallback('arg2', 'arg3');
// Inspect the mock's history
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith('arg1');
expect(mockCallback).toHaveBeenLastCalledWith('arg2', 'arg3');
// Access call details programmatically
console.log(mockCallback.mock.calls); // [['arg1'], ['arg2', 'arg3']]
console.log(mockCallback.mock.results); // [{type: 'return', value: undefined}, ...]
});
Configure mock functions to return specific values or implement custom behavior:
test('configuring mock return values', () => {
const mockFn = jest.fn();
// Simple return value
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn()).toBe(42); // Always returns 42
// Return value for a single call
mockFn.mockReturnValueOnce('first call');
mockFn.mockReturnValueOnce('second call');
mockFn.mockReturnValue('default');
expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');
expect(mockFn()).toBe('default');
expect(mockFn()).toBe('default');
});
test('mock implementation replaces the function body', () => {
const mockFn = jest.fn((a, b) => a * b);
expect(mockFn(5, 3)).toBe(15);
expect(mockFn(10, 2)).toBe(20);
// Change implementation dynamically
mockFn.mockImplementation((a, b) => a + b);
expect(mockFn(5, 3)).toBe(8);
});
test('mock async functions', async () => {
const mockAsync = jest.fn();
mockAsync.mockResolvedValue({data: 'success'});
const result = await mockAsync();
expect(result.data).toBe('success');
// For rejected promises
const mockFailing = jest.fn();
mockFailing.mockRejectedValue(new Error('Network error'));
await expect(mockFailing()).rejects.toThrow('Network error');
});
Spying on Existing Functions: jest.spyOn()
Sometimes you need to observe a real method without completely replacing it. jest.spyOn() creates a mock function that wraps the original implementation, letting you track calls while preserving actual behavior.
const calculator = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
calculate: function(operation, a, b) {
if (operation === 'add') return this.add(a, b);
if (operation === 'multiply') return this.multiply(a, b);
return null;
}
};
test('spies on add method while preserving its behavior', () => {
const spy = jest.spyOn(calculator, 'add');
const result = calculator.calculate('add', 3, 4);
expect(result).toBe(7);
expect(spy).toHaveBeenCalledWith(3, 4);
expect(spy).toHaveBeenCalledTimes(1);
// Restore the original function
spy.mockRestore();
});
test('spies can also replace implementation', () => {
const spy = jest.spyOn(calculator, 'multiply');
spy.mockImplementation(() => 999); // Override behavior
expect(calculator.multiply(2, 3)).toBe(999);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
expect(calculator.multiply(2, 3)).toBe(6); // Back to normal
});
Module Mocking
Jest can automatically mock entire modules, replacing every exported function with a mock. This is invaluable when testing code that depends on external services, databases, or APIs.
For partial module mocking, use jest.requireActual() to keep the real implementation for specific exports:
// mathUtils.js
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const complexCalculation = (x) => multiply(add(x, 2), 3);
module.exports = {add, multiply, complexCalculation};
// mathUtils.test.js
jest.mock('./mathUtils', () => {
const actual = jest.requireActual('./mathUtils');
return {
...actual, // Keep real add and multiply
complexCalculation: jest.fn() // Mock only complexCalculation
};
});
const {add, multiply, complexCalculation} = require('./mathUtils');
test('real implementations are preserved', () => {
expect(add(1, 2)).toBe(3); // Real add
expect(multiply(3, 4)).toBe(12); // Real multiply
});
test('complexCalculation is mocked', () => {
complexCalculation.mockReturnValue(100);
expect(complexCalculation(5)).toBe(100);
expect(complexCalculation).toHaveBeenCalledWith(5);
});
Manual Mocks
For complex modules (APIs, databases, third-party SDKs), create manual mocks in a __mocks__ directory. Jest will automatically use these mocks when jest.mock() is called.
Jest works seamlessly with React through the @testing-library/react ecosystem (or the older Enzyme library). The testing-library approach focuses on testing user behavior rather than implementation details.