Skip to content

Jest

Jest is a delightful JavaScript testing framework with a focus on simplicity.

Installation

# Install Jest
npm install --save-dev jest

# TypeScript support
npm install --save-dev @types/jest ts-jest

# For React
npm install --save-dev @testing-library/react @testing-library/jest-dom

Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/**/*.test.{js,jsx}'
  ],
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js'
  ]
};
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Basic Tests

// sum.js
function sum(a, b) {
  return a + b;
}

module.exports = sum;

// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

describe('sum function', () => {
  test('adds positive numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });

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

Matchers

// Equality
expect(value).toBe(4);                    // Exact equality (===)
expect(value).toEqual({ name: 'John' });  // Deep equality
expect(value).not.toBe(5);                // Not equal

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(0.1 + 0.2).toBeCloseTo(0.3);       // Floating point

// Strings
expect('team').not.toMatch(/I/);
expect('team').toMatch(/tea/);

// Arrays and iterables
expect(['apple', 'banana']).toContain('apple');
expect(new Set(['apple', 'banana'])).toContain('apple');

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

Async Testing

// Promises
test('async test with promises', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

// Async/Await
test('async test with async/await', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('async test with error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (error) {
    expect(error).toMatch('error');
  }
});

// Alternative error testing
test('async error test', async () => {
  await expect(fetchData()).rejects.toThrow('error');
});

Mocking

Mock Functions

// Create mock function
const mockFn = jest.fn();

// Mock implementation
const mockFn = jest.fn(x => x + 1);

// Test mock
test('mock function', () => {
  mockFn(1);
  mockFn(2);

  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(1);
  expect(mockFn).toHaveBeenLastCalledWith(2);
});

// Mock return values
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
mockFn.mockReturnValueOnce(100).mockReturnValueOnce(200);

// Mock resolved/rejected promises
mockFn.mockResolvedValue('success');
mockFn.mockRejectedValue(new Error('error'));

Mock Modules

// axios.js
import axios from 'axios';

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

// axios.test.js
import axios from 'axios';
import { fetchUser } from './axios';

jest.mock('axios');

test('fetches user', async () => {
  const user = { id: 1, name: 'John' };
  axios.get.mockResolvedValue({ data: user });

  const result = await fetchUser(1);

  expect(result).toEqual(user);
  expect(axios.get).toHaveBeenCalledWith('/users/1');
});

Setup and Teardown

// Before each test
beforeEach(() => {
  initializeDatabase();
});

// After each test
afterEach(() => {
  clearDatabase();
});

// Before all tests
beforeAll(() => {
  connectToDatabase();
});

// After all tests
afterAll(() => {
  disconnectFromDatabase();
});

// Scoped to describe block
describe('database tests', () => {
  beforeEach(() => {
    initializeDatabase();
  });

  test('test 1', () => {
    // Database initialized
  });
});

Snapshot Testing

// Component
function Link({ page, children }) {
  return <a href={page}>{children}</a>;
}

// Test
test('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://example.com">Example</Link>)
    .toJSON();

  expect(tree).toMatchSnapshot();
});

// Update snapshots
// npm test -- -u

Testing React Components

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from './Button';

test('renders button', () => {
  render(<Button>Click me</Button>);

  const button = screen.getByText('Click me');
  expect(button).toBeInTheDocument();
});

test('button click', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click me</Button>);

  const button = screen.getByText('Click me');
  fireEvent.click(button);

  expect(handleClick).toHaveBeenCalledTimes(1);
});

test('async component', async () => {
  render(<UserList />);

  const user = await screen.findByText('John Doe');
  expect(user).toBeInTheDocument();
});

Coverage

# Run with coverage
npm test -- --coverage

# Coverage thresholds in jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Watch Mode

# Watch mode
npm test -- --watch

# Watch all files
npm test -- --watchAll

# Commands in watch mode:
# › Press f to run only failed tests.
# › Press o to only run tests related to changed files.
# › Press p to filter by a filename regex pattern.
# › Press t to filter by a test name regex pattern.
# › Press q to quit watch mode.
# › Press Enter to trigger a test run.

Timer Mocks

jest.useFakeTimers();

test('waits 1 second', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000);

  // Fast-forward time
  jest.runAllTimers();

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

test('advances timers by time', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000);

  // Advance by 500ms
  jest.advanceTimersByTime(500);
  expect(callback).not.toHaveBeenCalled();

  // Advance by another 500ms
  jest.advanceTimersByTime(500);
  expect(callback).toHaveBeenCalled();
});

Custom Matchers

// Custom matcher
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false,
      };
    }
  },
});

test('numeric ranges', () => {
  expect(100).toBeWithinRange(90, 110);
  expect(101).not.toBeWithinRange(0, 100);
});

Test Structure Best Practices

describe('UserService', () => {
  describe('getUser', () => {
    test('returns user when user exists', async () => {
      // Arrange
      const userId = 1;
      const expectedUser = { id: 1, name: 'John' };

      // Act
      const result = await UserService.getUser(userId);

      // Assert
      expect(result).toEqual(expectedUser);
    });

    test('throws error when user does not exist', async () => {
      // Arrange
      const userId = 999;

      // Act & Assert
      await expect(UserService.getUser(userId))
        .rejects
        .toThrow('User not found');
    });
  });
});

Resources