← Back to DevBytes

Jest: Complete Testing Guide for Developers

Introduction to Jest

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:

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:

Getting Started with Jest

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Installation

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

Add a test script to your package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

For TypeScript projects, create a jest.config.js file:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['/src'],
  testMatch: [
    '**/__tests__/**/*.ts',
    '**/*.test.ts'
  ],
  moduleFileExtensions: ['ts', 'js', 'json', 'node'],
};

Your First Test

Create a file named sum.js in your project:

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'));

Array matchers verify array content:

expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect([{id: 1}, {id: 2}]).toEqual(
  expect.arrayContaining([{id: 1}])
);

Object matchers provide flexible partial matching:

const user = {name: 'Alice', age: 30, email: 'alice@example.com'};

expect(user).toHaveProperty('name', 'Alice');
expect(user).toMatchObject({name: 'Alice'}); // partial match
expect(user).toEqual(
  expect.objectContaining({name: 'Alice', age: expect.any(Number)})
);

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:

  1. All beforeAll hooks in the outer describe
  2. All beforeEach hooks, from outer to inner
  3. The test itself
  4. All afterEach hooks, from inner to outer
  5. All afterAll hooks in the outer describe
describe('outer', () => {
  beforeAll(() => console.log('outer beforeAll'));        // 1st
  beforeEach(() => console.log('outer beforeEach'));       // 3rd, 7th
  
  describe('inner', () => {
    beforeAll(() => console.log('inner beforeAll'));       // 2nd
    beforeEach(() => console.log('inner beforeEach'));     // 4th, 8th
    afterEach(() => console.log('inner afterEach'));       // 6th, 10th
    afterAll(() => console.log('inner afterAll'));         // 12th
    
    test('test 1', () => console.log('test 1'));           // 5th
    test('test 2', () => console.log('test 2'));           // 9th
  });
  
  afterEach(() => console.log('outer afterEach'));         // 11th
  afterAll(() => console.log('outer afterAll'));           // 13th
});

Testing Asynchronous Code

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:

test('fetchUser resolves correctly with resolves matcher', () => {
  return expect(fetchUser(5)).resolves.toEqual({
    id: 5,
    name: 'Bob'
  });
});

test('fetchUser rejects with rejects matcher', () => {
  return expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});

Async/Await

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.

// userService.js
const axios = require('axios');

async function getUser(id) {
  const response = await axios.get(`/api/users/${id}`);
  return response.data;
}

module.exports = {getUser};

// userService.test.js
jest.mock('axios'); // Auto-mock the entire axios module

const {getUser} = require('./userService');
const axios = require('axios');

test('getUser returns user data from API', async () => {
  // Configure the mocked axios.get
  axios.get.mockResolvedValue({
    data: {id: 1, name: 'Alice', email: 'alice@example.com'}
  });
  
  const user = await getUser(1);
  
  expect(user.name).toBe('Alice');
  expect(axios.get).toHaveBeenCalledWith('/api/users/1');
  expect(axios.get).toHaveBeenCalledTimes(1);
});

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.

// __mocks__/apiClient.js
const mockApiClient = {
  get: jest.fn(),
  post: jest.fn(),
  put: jest.fn(),
  delete: jest.fn(),
  
  // Simulate a successful GET response
  __setGetResponse(data) {
    this.get.mockResolvedValue({data});
  },
  
  // Reset all mock state
  __reset() {
    this.get.mockReset();
    this.post.mockReset();
    this.put.mockReset();
    this.delete.mockReset();
  }
};

module.exports = mockApiClient;

// orderService.test.js
jest.mock('./apiClient');
const apiClient = require('./apiClient');
const {createOrder} = require('./orderService');

beforeEach(() => {
  apiClient.__reset();
});

test('createOrder sends POST request and returns order', async () => {
  apiClient.post.mockResolvedValue({
    data: {id: 'order-123', status: 'confirmed'}
  });
  
  const order = await createOrder({item: 'Book'});
  
  expect(order.id).toBe('order-123');
  expect(apiClient.post).toHaveBeenCalledWith('/orders', {item: 'Book'});
});

Testing React Components

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.

Setup for React Testing

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

Create a test setup file to extend Jest's matchers with DOM-specific assertions:

// jest.setup.js
import '@testing-library/jest-dom';

// jest.config.js
module.exports = {
  setupFilesAfterSetup: ['/jest.setup.js'],
  testEnvironment: 'jsdom', // Required for DOM testing
};

Rendering and Querying Elements

// Button.jsx
import React from 'react';

function Button({label, onClick, disabled = false}) {
  return (
    
  );
}

export default Button;

// Button.test.jsx
import React from 'react';
import {render, screen, fireEvent} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';

describe('Button component', () => {
  test('renders with the correct label', () => {
    render(

Testing Component State Changes

// Counter.jsx
import React, {useState} from 'react';

function Counter({initialCount = 0}) {
  const [count, setCount] = useState(initialCount);
  
  return (
    

Count: {count}

); } export default Counter; // Counter.test.jsx import React from 'react'; import {render, screen} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Counter from './Counter'; describe('Counter component', () => { test('starts with initial count', () => { render(); expect(screen.getByText('5')).toBeInTheDocument(); }); test('increments count when Increment button is clicked', async () => { render(); const incrementBtn = screen.getByRole('button', {name: /increment/i}); await userEvent.click(incrementBtn); expect(screen.getByText('1')).toBeInTheDocument(); await userEvent.click(incrementBtn); await userEvent.click(incrementBtn); expect(screen.getByText('3')).toBeInTheDocument(); }); test('decrements count correctly', async () => { render(); const decrementBtn = screen.getByRole('button', {name: /decrement/i});

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles