TL;DR

  • Jest is a zero-config testing framework that includes assertions, mocking, and coverage
  • Matchers like toBe, toEqual, toContain make assertions readable
  • Mock functions with jest.fn(), modules with jest.mock(), timers with jest.useFakeTimers()
  • Async testing: use async/await, resolves/rejects, or callback done
  • Snapshot testing captures UI output — useful for React components

Best for: JavaScript/TypeScript developers, React/Vue/Node.js projects, teams wanting all-in-one testing Skip if: You need browser-based testing (use Playwright/Cypress instead) Read time: 15 minutes

Your test suite runs for 10 minutes. Half the tests are flaky. Nobody trusts the results anymore.

Jest changes this. It’s fast, reliable, and works out of the box. No configuration hell. No piecing together five different libraries.

This tutorial teaches Jest from scratch — matchers, mocking, async testing, and the patterns that make tests maintainable.

What is Jest?

Jest is a JavaScript testing framework created by Facebook. It runs your tests, provides assertions, mocks dependencies, and generates coverage reports — all in one package.

What Jest includes:

  • Test runner — finds and executes test files
  • Assertion libraryexpect() with built-in matchers
  • Mocking — mock functions, modules, timers
  • Coverage — built-in code coverage reports
  • Snapshot testing — capture and compare output
  • Watch mode — re-run tests on file changes

Installation and Setup

New Project

# Initialize project
npm init -y

# Install Jest
npm install --save-dev jest

# Add test script to package.json
npm pkg set scripts.test="jest"

TypeScript Project

npm install --save-dev jest typescript ts-jest @types/jest

# Initialize ts-jest config
npx ts-jest config:init

Create React App

Jest is already included. Just run:

npm test

Project Structure

my-project/
├── src/
│   ├── calculator.js
│   └── utils/
│       └── formatters.js
├── __tests__/
│   ├── calculator.test.js
│   └── utils/
│       └── formatters.test.js
├── jest.config.js
└── package.json

Jest finds tests in:

  • Files ending with .test.js or .spec.js
  • Files in __tests__ folders

Writing Your First Test

// src/calculator.js
function add(a, b) {
  return a + b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, divide };
// __tests__/calculator.test.js
const { add, divide } = require('../src/calculator');

describe('Calculator', () => {
  describe('add', () => {
    test('adds two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('adds negative numbers', () => {
      expect(add(-1, -1)).toBe(-2);
    });

    test('adds zero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    test('divides two numbers', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('throws error when dividing by zero', () => {
      expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    });
  });
});

Running Tests

# Run all tests
npm test

# Run specific file
npm test -- calculator.test.js

# Run tests matching pattern
npm test -- --testNamePattern="adds"

# Watch mode
npm test -- --watch

# With coverage
npm test -- --coverage

Matchers

Matchers are methods that check values. Jest has 50+ built-in matchers.

Common Matchers

// Equality
expect(2 + 2).toBe(4);                    // Strict equality (===)
expect({ a: 1 }).toEqual({ a: 1 });       // Deep equality

// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();

// Numbers
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);       // Floating point

// Strings
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');

// Arrays
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect(['a', 'b']).toEqual(expect.arrayContaining(['a']));

// Objects
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: 1 }).toMatchObject({ a: 1 });

// Exceptions
expect(() => { throw new Error('fail'); }).toThrow();
expect(() => { throw new Error('fail'); }).toThrow('fail');
expect(() => { throw new Error('fail'); }).toThrow(Error);

Negating Matchers

Add .not before any matcher:

expect(5).not.toBe(3);
expect([1, 2]).not.toContain(3);
expect({ a: 1 }).not.toHaveProperty('b');

Testing Async Code

Async/Await

// src/api.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('User not found');
  }
  return response.json();
}

module.exports = { fetchUser };
// __tests__/api.test.js
test('fetches user successfully', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('John');
});

test('throws error for invalid user', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

Promises with resolves/rejects

test('resolves to user data', () => {
  return expect(fetchUser(1)).resolves.toMatchObject({ name: 'John' });
});

test('rejects for missing user', () => {
  return expect(fetchUser(999)).rejects.toThrow();
});

Callback Style (done)

function fetchDataWithCallback(callback) {
  setTimeout(() => {
    callback({ data: 'result' });
  }, 100);
}

test('calls callback with data', (done) => {
  function callback(result) {
    expect(result.data).toBe('result');
    done();  // Test waits until done() is called
  }
  fetchDataWithCallback(callback);
});

Mocking

Mocking replaces real implementations with controlled substitutes.

Mock Functions (jest.fn)

test('mock function tracks calls', () => {
  const mockCallback = jest.fn(x => x + 1);

  [1, 2, 3].forEach(mockCallback);

  // Check call count
  expect(mockCallback).toHaveBeenCalledTimes(3);

  // Check specific calls
  expect(mockCallback).toHaveBeenCalledWith(1);
  expect(mockCallback).toHaveBeenLastCalledWith(3);

  // Check return values
  expect(mockCallback.mock.results[0].value).toBe(2);
});

Mock Return Values

const mock = jest.fn();

// Return different values on successive calls
mock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce(20)
  .mockReturnValue(30);

console.log(mock()); // 10
console.log(mock()); // 20
console.log(mock()); // 30
console.log(mock()); // 30

// Mock resolved/rejected promises
const asyncMock = jest.fn()
  .mockResolvedValueOnce({ success: true })
  .mockRejectedValueOnce(new Error('Failed'));

Mocking Modules

// Mock entire module
jest.mock('./api');

const { fetchUser } = require('./api');

// Set up mock implementation
fetchUser.mockResolvedValue({ id: 1, name: 'Mock User' });

test('uses mocked API', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Mock User');
});

Mocking with Factory

jest.mock('./database', () => ({
  connect: jest.fn().mockResolvedValue(true),
  query: jest.fn().mockResolvedValue([{ id: 1 }]),
  close: jest.fn()
}));

Spying on Methods

const video = {
  play() {
    return true;
  }
};

test('spy on method', () => {
  const spy = jest.spyOn(video, 'play');

  video.play();

  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveReturnedWith(true);

  spy.mockRestore();  // Restore original implementation
});

Mocking Timers

jest.useFakeTimers();

function delayedGreeting(callback) {
  setTimeout(() => callback('Hello'), 1000);
}

test('calls callback after delay', () => {
  const callback = jest.fn();

  delayedGreeting(callback);

  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1000);  // Fast-forward time

  expect(callback).toHaveBeenCalledWith('Hello');
});

// Or run all timers
test('with runAllTimers', () => {
  const callback = jest.fn();

  delayedGreeting(callback);
  jest.runAllTimers();

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

Setup and Teardown

describe('Database tests', () => {
  let db;

  // Runs once before all tests in this describe
  beforeAll(async () => {
    db = await connectToDatabase();
  });

  // Runs once after all tests
  afterAll(async () => {
    await db.close();
  });

  // Runs before each test
  beforeEach(async () => {
    await db.clear();
  });

  // Runs after each test
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('inserts record', async () => {
    await db.insert({ name: 'Test' });
    const records = await db.findAll();
    expect(records).toHaveLength(1);
  });
});

Snapshot Testing

Snapshots capture output and detect unintended changes.

// src/formatUser.js
function formatUser(user) {
  return {
    displayName: `${user.firstName} ${user.lastName}`,
    email: user.email.toLowerCase(),
    initials: `${user.firstName[0]}${user.lastName[0]}`
  };
}

module.exports = { formatUser };
// __tests__/formatUser.test.js
const { formatUser } = require('../src/formatUser');

test('formats user correctly', () => {
  const user = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'John.Doe@Example.com'
  };

  expect(formatUser(user)).toMatchSnapshot();
});

First run creates __snapshots__/formatUser.test.js.snap:

exports[`formats user correctly 1`] = `
{
  "displayName": "John Doe",
  "email": "john.doe@example.com",
  "initials": "JD"
}
`;

If output changes, test fails. Update snapshots:

npm test -- --updateSnapshot
# or
npm test -- -u

Inline Snapshots

test('formats user inline', () => {
  const user = { firstName: 'John', lastName: 'Doe', email: 'J@E.com' };

  expect(formatUser(user)).toMatchInlineSnapshot(`
    {
      "displayName": "John Doe",
      "email": "j@e.com",
      "initials": "JD"
    }
  `);
});

Code Coverage

# Generate coverage report
npm test -- --coverage

# With specific thresholds
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80}}'

Coverage Configuration

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

Jest Configuration

// jest.config.js
module.exports = {
  // Test environment
  testEnvironment: 'node',  // or 'jsdom' for browser

  // File patterns
  testMatch: ['**/__tests__/**/*.test.js'],
  testPathIgnorePatterns: ['/node_modules/'],

  // Transform files
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  },

  // Module resolution
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less)$': 'identity-obj-proxy'
  },

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],

  // Timeouts
  testTimeout: 10000,

  // Parallelization
  maxWorkers: '50%'
};

Best Practices

1. One Assertion Per Test (Usually)

// Bad: multiple unrelated assertions
test('user validation', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidEmail('invalid')).toBe(false);
  expect(isValidName('John')).toBe(true);
});

// Good: separate tests
test('accepts valid email', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

test('rejects invalid email', () => {
  expect(isValidEmail('invalid')).toBe(false);
});

2. Descriptive Test Names

// Bad
test('test1', () => { ... });

// Good
test('returns null when user ID is not found', () => { ... });

3. Arrange-Act-Assert Pattern

test('calculates total with discount', () => {
  // Arrange
  const cart = { items: [{ price: 100 }, { price: 50 }] };
  const discount = 0.1;

  // Act
  const total = calculateTotal(cart, discount);

  // Assert
  expect(total).toBe(135);
});

4. Avoid Testing Implementation Details

// Bad: tests internal state
test('sets internal flag', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter._count).toBe(1);  // Testing private property
});

// Good: tests public behavior
test('increments count', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter.getCount()).toBe(1);  // Testing public method
});

5. Use beforeEach for Common Setup

describe('ShoppingCart', () => {
  let cart;

  beforeEach(() => {
    cart = new ShoppingCart();
    cart.addItem({ id: 1, price: 10 });
  });

  test('calculates subtotal', () => {
    expect(cart.getSubtotal()).toBe(10);
  });

  test('applies discount', () => {
    cart.applyDiscount(0.1);
    expect(cart.getTotal()).toBe(9);
  });
});

AI-Assisted Jest Testing

AI tools can accelerate test writing when used appropriately.

What AI does well:

  • Generate test cases from function signatures
  • Create mock data matching specific shapes
  • Write boilerplate for common patterns
  • Suggest edge cases you might miss

What still needs humans:

  • Deciding what’s worth testing
  • Verifying tests actually test the right thing
  • Writing tests for complex business logic
  • Debugging failing tests

Useful prompt:

I have this function:
function validateOrder(order) {
  if (!order.items?.length) return { valid: false, error: 'No items' };
  if (order.items.some(i => i.quantity <= 0)) return { valid: false, error: 'Invalid quantity' };
  if (order.total !== order.items.reduce((sum, i) => sum + i.price * i.quantity, 0)) {
    return { valid: false, error: 'Total mismatch' };
  }
  return { valid: true };
}

Generate Jest tests covering:
- Valid order
- Empty items array
- Missing items property
- Zero/negative quantity
- Mismatched total

FAQ

What is Jest used for?

Jest is a JavaScript testing framework used for unit tests, integration tests, and snapshot testing. It provides a test runner, assertion library, mocking utilities, and code coverage — all in one package. Jest works with React, Vue, Angular, Node.js, and any JavaScript or TypeScript project.

Is Jest only for React?

No. While Jest was created by Facebook and is popular in React projects, it works with any JavaScript or TypeScript codebase. Jest tests Node.js backend code, Vue components, Angular applications, and vanilla JavaScript equally well. React Testing Library is a separate package that complements Jest for React-specific testing.

What’s the difference between Jest and Mocha?

Jest is an all-in-one testing framework with built-in assertions (expect), mocking (jest.fn), and coverage. Mocha is a test runner that requires separate libraries: Chai for assertions, Sinon for mocking, nyc for coverage. Jest is easier to set up and configure; Mocha offers more flexibility and customization. For new projects, Jest is usually the simpler choice.

How do I mock API calls in Jest?

Several approaches work:

  1. jest.mock() — mock the entire fetch/axios module
  2. jest.spyOn() — spy on and mock specific methods
  3. Manual mocks — create __mocks__ folder with mock implementations
  4. MSW (Mock Service Worker) — intercept network requests for realistic API mocking

For simple cases, jest.mock() is sufficient. For complex applications, MSW provides more realistic behavior.

Official Resources

See Also