Skip to content

Jest Cheatsheet

Jest - Delightful JavaScript Testing

Jest is a delightful JavaScript testing framework with a focus on simplicity. It works out of the box for most JavaScript projects and provides features like snapshot testing, built-in test runner, assertion library, and powerful mocking capabilities.

Table of Contents

Installation

Basic Installation

# Install Jest
npm install --save-dev jest

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

# Install with Babel support
npm install --save-dev jest babel-jest @babel/core @babel/preset-env

# Global installation (not recommended)
npm install -g jest

React Projects

# Create React App (Jest included)
npx create-react-app my-app
cd my-app

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

# React Native
npm install --save-dev jest react-test-renderer

Package.json Configuration

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --watchAll=false"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "@testing-library/jest-dom": "^5.16.0"
  }
}

Project Structure

my-project/
├── src/
│   ├── components/
│   │   ├── Button.js
│   │   └── Button.test.js
│   ├── utils/
│   │   ├── math.js
│   │   └── math.test.js
│   └── __tests__/
│       └── integration.test.js
├── __mocks__/
│   └── fileMock.js
├── jest.config.js
└── package.json

Getting Started

First Test

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}
// math.test.js
import { add, subtract, multiply, divide } from './math';

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

  test('subtracts 5 - 3 to equal 2', () => {
    expect(subtract(5, 3)).toBe(2);
  });

  test('multiplies 3 * 4 to equal 12', () => {
    expect(multiply(3, 4)).toBe(12);
  });

  test('divides 8 / 2 to equal 4', () => {
    expect(divide(8, 2)).toBe(4);
  });

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

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run specific test file
npm test math.test.js

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

# Run tests with coverage
npm run test:coverage

# Run tests in CI mode
npm run test:ci

Test Organization

// Grouping tests with describe
describe('Calculator', () => {
  describe('Addition', () => {
    test('should add positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('should add negative numbers', () => {
      expect(add(-2, -3)).toBe(-5);
    });

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

  describe('Division', () => {
    test('should divide positive numbers', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('should handle division by zero', () => {
      expect(() => divide(10, 0)).toThrow();
    });
  });
});

Basic Testing

Test Structure

// Basic test structure
test('description of what is being tested', () => {
  // Arrange - set up test data
  const input = 'hello';
  const expected = 'HELLO';

  // Act - execute the function
  const result = input.toUpperCase();

  // Assert - verify the result
  expect(result).toBe(expected);
});

// Alternative syntax
it('should convert string to uppercase', () => {
  expect('hello'.toUpperCase()).toBe('HELLO');
});

Setup and Teardown

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

  // Run before all tests in this describe block
  beforeAll(async () => {
    database = await connectToDatabase();
  });

  // Run after all tests in this describe block
  afterAll(async () => {
    await database.close();
  });

  // Run before each test
  beforeEach(() => {
    database.clear();
  });

  // Run after each test
  afterEach(() => {
    database.cleanup();
  });

  test('should save user', async () => {
    const user = { name: 'John', email: 'john@example.com' };
    await database.save(user);

    const savedUser = await database.findByEmail('john@example.com');
    expect(savedUser).toEqual(user);
  });
});

Skipping and Focusing Tests

// Skip tests
describe.skip('Skipped test suite', () => {
  test('this will not run', () => {
    expect(true).toBe(false);
  });
});

test.skip('skipped test', () => {
  expect(true).toBe(false);
});

// Focus on specific tests (only these will run)
describe.only('Focused test suite', () => {
  test('this will run', () => {
    expect(true).toBe(true);
  });
});

test.only('focused test', () => {
  expect(true).toBe(true);
});

// Conditional tests
const runIntegrationTests = process.env.NODE_ENV === 'test';

(runIntegrationTests ? describe : describe.skip)('Integration tests', () => {
  test('should integrate with external service', () => {
    // Integration test code
  });
});

Parameterized Tests

// Test with multiple inputs
describe('isPrime function', () => {
  test.each([
    [2, true],
    [3, true],
    [4, false],
    [5, true],
    [6, false],
    [7, true],
    [8, false],
    [9, false],
    [10, false],
    [11, true],
  ])('isPrime(%i) should return %s', (input, expected) => {
    expect(isPrime(input)).toBe(expected);
  });
});

// Test with objects
describe('user validation', () => {
  test.each([
    { name: 'John', email: 'john@example.com', valid: true },
    { name: '', email: 'john@example.com', valid: false },
    { name: 'John', email: 'invalid-email', valid: false },
    { name: 'John', email: '', valid: false },
  ])('validateUser($name, $email) should return $valid', ({ name, email, valid }) => {
    expect(validateUser({ name, email })).toBe(valid);
  });
});

Matchers

Basic Matchers

describe('Basic matchers', () => {
  test('equality matchers', () => {
    expect(2 + 2).toBe(4); // Exact equality (Object.is)
    expect({ name: 'John' }).toEqual({ name: 'John' }); // Deep equality
    expect({ name: 'John' }).not.toBe({ name: 'John' }); // Different objects
  });

  test('truthiness matchers', () => {
    expect(true).toBeTruthy();
    expect(false).toBeFalsy();
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
    expect('hello').toBeDefined();
  });

  test('number matchers', () => {
    expect(2 + 2).toBeGreaterThan(3);
    expect(2 + 2).toBeGreaterThanOrEqual(4);
    expect(2 + 2).toBeLessThan(5);
    expect(2 + 2).toBeLessThanOrEqual(4);
    expect(0.1 + 0.2).toBeCloseTo(0.3); // Floating point
  });
});

String Matchers

describe('String matchers', () => {
  test('string matching', () => {
    expect('Hello World').toMatch(/World/);
    expect('Hello World').toMatch('World');
    expect('user@example.com').toMatch(/^[\w\.-]+@[\w\.-]+\.\w+$/);

    expect('Hello World').toContain('World');
    expect('Hello World').not.toContain('Goodbye');

    expect('Hello World').toHaveLength(11);
    expect('').toHaveLength(0);
  });
});

Array and Object Matchers

describe('Array and object matchers', () => {
  test('array matchers', () => {
    const fruits = ['apple', 'banana', 'orange'];

    expect(fruits).toContain('banana');
    expect(fruits).toHaveLength(3);
    expect(fruits).toEqual(['apple', 'banana', 'orange']);
    expect(fruits).toEqual(expect.arrayContaining(['apple', 'banana']));
  });

  test('object matchers', () => {
    const user = {
      id: 1,
      name: 'John',
      email: 'john@example.com',
      profile: {
        age: 30,
        city: 'New York'
      }
    };

    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('profile.age', 30);
    expect(user).toMatchObject({
      name: 'John',
      email: 'john@example.com'
    });

    expect(user).toEqual(expect.objectContaining({
      name: 'John',
      id: expect.any(Number)
    }));
  });
});

Exception Matchers

describe('Exception matchers', () => {
  test('function throws', () => {
    const throwError = () => {
      throw new Error('Something went wrong');
    };

    expect(throwError).toThrow();
    expect(throwError).toThrow('Something went wrong');
    expect(throwError).toThrow(/wrong/);
    expect(throwError).toThrow(Error);
  });

  test('async function throws', async () => {
    const asyncThrowError = async () => {
      throw new Error('Async error');
    };

    await expect(asyncThrowError()).rejects.toThrow('Async error');
  });
});

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,
      };
    }
  },
});

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

// Async custom matcher
expect.extend({
  async toBeValidUser(received) {
    const isValid = await validateUserAsync(received);
    if (isValid) {
      return {
        message: () => `expected ${received} not to be a valid user`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be a valid user`,
        pass: false,
      };
    }
  },
});

Async Testing

Promises

describe('Promise testing', () => {
  // Return promise
  test('async function returns promise', () => {
    return fetchUser(1).then(user => {
      expect(user.name).toBe('John');
    });
  });

  // Async/await
  test('async function with async/await', async () => {
    const user = await fetchUser(1);
    expect(user.name).toBe('John');
  });

  // Testing rejections
  test('async function rejects', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('User not found');
  });

  // Testing both resolve and reject
  test('promise resolves', () => {
    return expect(fetchUser(1)).resolves.toEqual({
      id: 1,
      name: 'John'
    });
  });
});

Callbacks

describe('Callback testing', () => {
  // Callback with done
  test('callback function', (done) => {
    function callback(data) {
      try {
        expect(data).toBe('callback data');
        done();
      } catch (error) {
        done(error);
      }
    }

    fetchDataCallback(callback);
  });

  // Testing error callbacks
  test('error callback', (done) => {
    function errorCallback(error) {
      try {
        expect(error.message).toBe('Something went wrong');
        done();
      } catch (err) {
        done(err);
      }
    }

    fetchDataWithError(errorCallback);
  });
});

Timers and Delays

describe('Timer testing', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  test('delayed function', () => {
    const callback = jest.fn();

    setTimeout(callback, 1000);

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

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

  test('interval function', () => {
    const callback = jest.fn();

    setInterval(callback, 1000);

    // Fast-forward multiple intervals
    jest.advanceTimersByTime(3000);

    expect(callback).toHaveBeenCalledTimes(3);
  });

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

    setTimeout(callback, 1000);
    setTimeout(callback, 2000);

    jest.runAllTimers();

    expect(callback).toHaveBeenCalledTimes(2);
  });
});

Mocking

Function Mocking

describe('Function mocking', () => {
  test('mock function', () => {
    const mockFn = jest.fn();

    mockFn('arg1', 'arg2');
    mockFn('arg3');

    expect(mockFn).toHaveBeenCalledTimes(2);
    expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
    expect(mockFn).toHaveBeenLastCalledWith('arg3');
  });

  test('mock return values', () => {
    const mockFn = jest.fn();

    mockFn.mockReturnValue(42);
    expect(mockFn()).toBe(42);

    mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
    expect(mockFn()).toBe(1);
    expect(mockFn()).toBe(2);
    expect(mockFn()).toBe(42); // Default return value
  });

  test('mock implementation', () => {
    const mockFn = jest.fn((x, y) => x + y);

    expect(mockFn(1, 2)).toBe(3);
    expect(mockFn).toHaveBeenCalledWith(1, 2);
  });
});

Module Mocking

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

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

export async function createUser(userData) {
  const response = await axios.post('/api/users', userData);
  return response.data;
}

// userService.test.js
import axios from 'axios';
import { getUser, createUser } from './userService';

// Mock the entire axios module
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('User Service', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should fetch user', async () => {
    const userData = { id: 1, name: 'John' };
    mockedAxios.get.mockResolvedValue({ data: userData });

    const user = await getUser(1);

    expect(user).toEqual(userData);
    expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/1');
  });

  test('should create user', async () => {
    const newUser = { name: 'Jane', email: 'jane@example.com' };
    const createdUser = { id: 2, ...newUser };

    mockedAxios.post.mockResolvedValue({ data: createdUser });

    const user = await createUser(newUser);

    expect(user).toEqual(createdUser);
    expect(mockedAxios.post).toHaveBeenCalledWith('/api/users', newUser);
  });
});

Partial Mocking

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

// calculator.js
import * as math from './math';

export function calculate(operation, a, b) {
  switch (operation) {
    case 'add':
      return math.add(a, b);
    case 'subtract':
      return math.subtract(a, b);
    case 'multiply':
      return math.multiply(a, b);
    default:
      throw new Error('Unknown operation');
  }
}

// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';

// Partial mock - only mock specific functions
jest.mock('./math', () => ({
  ...jest.requireActual('./math'),
  add: jest.fn(),
}));

const mockedMath = math as jest.Mocked<typeof math>;

describe('Calculator', () => {
  test('should use mocked add function', () => {
    mockedMath.add.mockReturnValue(100);

    const result = calculate('add', 2, 3);

    expect(result).toBe(100);
    expect(mockedMath.add).toHaveBeenCalledWith(2, 3);
  });

  test('should use real subtract function', () => {
    const result = calculate('subtract', 5, 3);
    expect(result).toBe(2); // Real implementation
  });
});

Spy Functions

describe('Spy functions', () => {
  test('spy on object method', () => {
    const user = {
      getName: () => 'John',
      setName: (name) => { this.name = name; }
    };

    const spy = jest.spyOn(user, 'getName');

    const name = user.getName();

    expect(spy).toHaveBeenCalled();
    expect(name).toBe('John');

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

  test('spy on console.log', () => {
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

    console.log('Hello, World!');

    expect(consoleSpy).toHaveBeenCalledWith('Hello, World!');

    consoleSpy.mockRestore();
  });
});

Mock Files

// __mocks__/fs.js
const fs = jest.createMockFromModule('fs');

let mockFiles = Object.create(null);

function __setMockFiles(newMockFiles) {
  mockFiles = Object.create(null);
  for (const file in newMockFiles) {
    const dir = path.dirname(file);

    if (!mockFiles[dir]) {
      mockFiles[dir] = [];
    }
    mockFiles[dir].push(path.basename(file));
  }
}

function readFileSync(filePath) {
  return mockFiles[filePath] || '';
}

fs.__setMockFiles = __setMockFiles;
fs.readFileSync = readFileSync;

module.exports = fs;

// fileReader.test.js
import fs from 'fs';
import { readConfigFile } from './fileReader';

jest.mock('fs');

describe('File Reader', () => {
  test('should read config file', () => {
    fs.__setMockFiles({
      '/config/app.json': '{"name": "MyApp"}'
    });

    const config = readConfigFile('/config/app.json');

    expect(config).toEqual({ name: 'MyApp' });
  });
});

Snapshot Testing

Basic Snapshots

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

export function Button({ children, onClick, variant = 'primary' }) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// Button.test.js
import React from 'react';
import { render } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  test('should match snapshot', () => {
    const { container } = render(
      <Button variant="primary">Click me</Button>
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('should match snapshot with different variant', () => {
    const { container } = render(
      <Button variant="secondary">Cancel</Button>
    );

    expect(container.firstChild).toMatchSnapshot();
  });
});

Inline Snapshots

describe('Inline snapshots', () => {
  test('should match inline snapshot', () => {
    const user = {
      id: 1,
      name: 'John',
      createdAt: new Date('2023-01-01')
    };

    expect(user).toMatchInlineSnapshot(`
      Object {
        "createdAt": 2023-01-01T00:00:00.000Z,
        "id": 1,
        "name": "John",
      }
    `);
  });
});

Custom Snapshot Serializers

// Custom serializer for Date objects
expect.addSnapshotSerializer({
  test: (val) => val instanceof Date,
  print: (val) => `Date("${val.toISOString()}")`,
});

// Custom serializer for React elements
expect.addSnapshotSerializer({
  test: (val) => val && val.$$typeof === Symbol.for('react.element'),
  print: (val, serialize) => {
    const { type, props } = val;
    const { children, ...restProps } = props;

    return `<${type}${Object.keys(restProps).length ? ' ' + serialize(restProps) : ''}>${
      children ? serialize(children) : ''
    }</${type}>`;
  },
});

Property Matchers

describe('Property matchers', () => {
  test('should match snapshot with dynamic values', () => {
    const user = {
      id: Math.random(),
      name: 'John',
      createdAt: new Date(),
      profile: {
        lastLogin: new Date(),
        sessionId: 'abc123'
      }
    };

    expect(user).toMatchSnapshot({
      id: expect.any(Number),
      createdAt: expect.any(Date),
      profile: {
        lastLogin: expect.any(Date),
        sessionId: expect.any(String)
      }
    });
  });
});

Configuration

Jest Configuration File

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

  // Test file patterns
  testMatch: [
    '**/__tests__/**/*.(js|jsx|ts|tsx)',
    '**/*.(test|spec).(js|jsx|ts|tsx)'
  ],

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],

  // Module name mapping
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$': '<rootDir>/__mocks__/fileMock.js'
  },

  // Transform files
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
    '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js'
  },

  // Coverage
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/serviceWorker.js'
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },

  // Ignore patterns
  testPathIgnorePatterns: [
    '/node_modules/',
    '/build/',
    '/dist/'
  ],

  // Module directories
  moduleDirectories: ['node_modules', '<rootDir>/src'],

  // Globals
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.json'
    }
  }
};

TypeScript Configuration

// jest.config.js for TypeScript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',

  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },

  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

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

  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],

  globals: {
    'ts-jest': {
      tsconfig: {
        jsx: 'react-jsx'
      }
    }
  }
};

Setup Files

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

// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  unobserve() {}
};

// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

// Global test utilities
global.testUtils = {
  createMockUser: () => ({
    id: 1,
    name: 'Test User',
    email: 'test@example.com'
  })
};

Environment Variables

// jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/config/jest/setEnvVars.js'],
};

// config/jest/setEnvVars.js
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://localhost:3001';
process.env.FEATURE_FLAG_NEW_UI = 'true';

React Testing

Component Testing

// UserProfile.js
import React, { useState, useEffect } from 'react';

export function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('User not found');
        }
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

// UserProfile.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

// Mock fetch
global.fetch = jest.fn();

describe('UserProfile', () => {
  beforeEach(() => {
    fetch.mockClear();
  });

  test('should display loading state', () => {
    fetch.mockImplementation(() => new Promise(() => {})); // Never resolves

    render(<UserProfile userId={1} />);

    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('should display user data', async () => {
    const mockUser = {
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
      age: 30
    };

    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser
    });

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });

    expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
    expect(screen.getByText('Age: 30')).toBeInTheDocument();
  });

  test('should display error message', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'));

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('Error: Network error')).toBeInTheDocument();
    });
  });
});

Event Testing

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

export function Counter({ initialValue = 0, onCountChange }) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    onCountChange?.(newCount);
  };

  const decrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    onCountChange?.(newCount);
  };

  const reset = () => {
    setCount(initialValue);
    onCountChange?.(initialValue);
  };

  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

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

describe('Counter', () => {
  test('should increment count', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    const incrementButton = screen.getByText('+');
    await user.click(incrementButton);

    expect(screen.getByTestId('count')).toHaveTextContent('1');
  });

  test('should call onCountChange when count changes', async () => {
    const user = userEvent.setup();
    const onCountChange = jest.fn();

    render(<Counter onCountChange={onCountChange} />);

    const incrementButton = screen.getByText('+');
    await user.click(incrementButton);

    expect(onCountChange).toHaveBeenCalledWith(1);
  });

  test('should reset to initial value', async () => {
    const user = userEvent.setup();
    render(<Counter initialValue={5} />);

    const incrementButton = screen.getByText('+');
    const resetButton = screen.getByText('Reset');

    await user.click(incrementButton);
    expect(screen.getByTestId('count')).toHaveTextContent('6');

    await user.click(resetButton);
    expect(screen.getByTestId('count')).toHaveTextContent('5');
  });
});

Hook Testing

// useCounter.js
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  test('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter());

    expect(result.current.count).toBe(0);
  });

  test('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10));

    expect(result.current.count).toBe(10);
  });

  test('should increment count', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  test('should decrement count', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  test('should reset count', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    expect(result.current.count).toBe(12);

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});

Code Coverage

Coverage Configuration

// jest.config.js
module.exports = {
  collectCoverage: true,

  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/reportWebVitals.js',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}'
  ],

  coverageDirectory: 'coverage',

  coverageReporters: [
    'text',
    'lcov',
    'html',
    'json'
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    './src/components/': {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90
    }
  }
};

Coverage Scripts

{
  "scripts": {
    "test:coverage": "jest --coverage",
    "test:coverage:watch": "jest --coverage --watchAll",
    "test:coverage:ci": "jest --coverage --ci --watchAll=false",
    "coverage:open": "open coverage/lcov-report/index.html"
  }
}

Ignoring Coverage

// Ignore specific lines
function myFunction() {
  /* istanbul ignore next */
  if (process.env.NODE_ENV === 'development') {
    console.log('Development mode');
  }

  return 'result';
}

// Ignore entire function
/* istanbul ignore next */
function debugFunction() {
  console.log('Debug info');
}

// Ignore else branch
function processValue(value) {
  if (value) {
    return value.toUpperCase();
  }
  /* istanbul ignore else */
  else {
    return '';
  }
}

Advanced Features

Custom Test Environment

// jest-environment-custom.js
const { TestEnvironment } = require('jest-environment-jsdom');

class CustomTestEnvironment extends TestEnvironment {
  constructor(config, context) {
    super(config, context);

    // Add custom globals
    this.global.customGlobal = 'test-value';
  }

  async setup() {
    await super.setup();

    // Custom setup logic
    this.global.mockDatabase = {
      users: [],
      addUser: (user) => this.global.mockDatabase.users.push(user),
      getUser: (id) => this.global.mockDatabase.users.find(u => u.id === id)
    };
  }

  async teardown() {
    // Custom teardown logic
    this.global.mockDatabase = null;

    await super.teardown();
  }
}

module.exports = CustomTestEnvironment;

// jest.config.js
module.exports = {
  testEnvironment: './jest-environment-custom.js'
};

Custom Reporters

// custom-reporter.js
class CustomReporter {
  constructor(globalConfig, options) {
    this._globalConfig = globalConfig;
    this._options = options;
  }

  onRunStart(results, options) {
    console.log('🚀 Starting test run...');
  }

  onTestStart(test) {
    console.log(`▶️  Running ${test.path}`);
  }

  onTestResult(test, testResult, aggregatedResult) {
    if (testResult.numFailingTests > 0) {
      console.log(`❌ ${test.path} - ${testResult.numFailingTests} failed`);
    } else {
      console.log(`✅ ${test.path} - all tests passed`);
    }
  }

  onRunComplete(contexts, results) {
    console.log(`🏁 Test run complete: ${results.numPassedTests} passed, ${results.numFailedTests} failed`);
  }
}

module.exports = CustomReporter;

// jest.config.js
module.exports = {
  reporters: [
    'default',
    ['./custom-reporter.js', { option1: 'value1' }]
  ]
};

Global Setup and Teardown

// jest.config.js
module.exports = {
  globalSetup: './config/jest/globalSetup.js',
  globalTeardown: './config/jest/globalTeardown.js'
};

// config/jest/globalSetup.js
module.exports = async () => {
  console.log('🔧 Global setup');

  // Start test database
  global.__TEST_DB__ = await startTestDatabase();

  // Start mock server
  global.__MOCK_SERVER__ = await startMockServer();
};

// config/jest/globalTeardown.js
module.exports = async () => {
  console.log('🧹 Global teardown');

  // Stop test database
  await global.__TEST_DB__.stop();

  // Stop mock server
  await global.__MOCK_SERVER__.stop();
};

Watch Plugins

// watch-plugin-custom.js
class CustomWatchPlugin {
  constructor({ stdin, stdout, config, testPathPattern }) {
    this._stdin = stdin;
    this._stdout = stdout;
    this._config = config;
    this._testPathPattern = testPathPattern;
  }

  apply(jestHooks) {
    jestHooks.onFileChange(({ projects }) => {
      console.log('📁 Files changed, running tests...');
    });
  }

  getUsageInfo() {
    return {
      key: 'c',
      prompt: 'clear console'
    };
  }

  run(globalConfig, updateConfigAndRun) {
    console.clear();
    return Promise.resolve();
  }
}

module.exports = CustomWatchPlugin;

// jest.config.js
module.exports = {
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
    './watch-plugin-custom.js'
  ]
};

Testing Patterns

Page Object Model

// pageObjects/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
  }

  get emailInput() {
    return this.page.getByLabelText(/email/i);
  }

  get passwordInput() {
    return this.page.getByLabelText(/password/i);
  }

  get submitButton() {
    return this.page.getByRole('button', { name: /login/i });
  }

  get errorMessage() {
    return this.page.getByTestId('error-message');
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectErrorMessage(message) {
    await expect(this.errorMessage).toHaveText(message);
  }
}

// Login.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginPage } from './pageObjects/LoginPage';
import { Login } from './Login';

describe('Login', () => {
  test('should display error for invalid credentials', async () => {
    const user = userEvent.setup();
    render(<Login />);

    const loginPage = new LoginPage(screen);

    await loginPage.login('invalid@email.com', 'wrongpassword');
    await loginPage.expectErrorMessage('Invalid credentials');
  });
});

Test Factories

// factories/userFactory.js
export const createUser = (overrides = {}) => ({
  id: Math.floor(Math.random() * 1000),
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
  isActive: true,
  createdAt: new Date(),
  ...overrides
});

export const createUsers = (count, overrides = {}) => 
  Array.from({ length: count }, (_, index) => 
    createUser({ id: index + 1, ...overrides })
  );

// Usage in tests
import { createUser, createUsers } from './factories/userFactory';

describe('User Service', () => {
  test('should process user data', () => {
    const user = createUser({ name: 'Jane Doe', age: 25 });

    const result = processUser(user);

    expect(result.displayName).toBe('Jane Doe (25)');
  });

  test('should handle multiple users', () => {
    const users = createUsers(5, { isActive: true });

    const activeUsers = filterActiveUsers(users);

    expect(activeUsers).toHaveLength(5);
  });
});

Test Utilities

// testUtils/renderWithProviders.js
import React from 'react';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/theme';

export function renderWithProviders(
  ui,
  {
    initialEntries = ['/'],
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false }
      }
    }),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return (
      <BrowserRouter>
        <QueryClientProvider client={queryClient}>
          <ThemeProvider theme={theme}>
            {children}
          </ThemeProvider>
        </QueryClientProvider>
      </BrowserRouter>
    );
  }

  return render(ui, { wrapper: Wrapper, ...renderOptions });
}

// Usage
import { renderWithProviders } from './testUtils/renderWithProviders';

test('should render with all providers', () => {
  renderWithProviders(<MyComponent />);
  // Test implementation
});

Custom Matchers for Domain Logic

// matchers/userMatchers.js
expect.extend({
  toBeValidUser(received) {
    const pass = 
      received &&
      typeof received.id === 'number' &&
      typeof received.name === 'string' &&
      received.name.length > 0 &&
      typeof received.email === 'string' &&
      received.email.includes('@');

    if (pass) {
      return {
        message: () => `expected ${JSON.stringify(received)} not to be a valid user`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${JSON.stringify(received)} to be a valid user`,
        pass: false,
      };
    }
  },

  toHavePermission(received, permission) {
    const pass = received.permissions && received.permissions.includes(permission);

    if (pass) {
      return {
        message: () => `expected user not to have permission "${permission}"`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected user to have permission "${permission}"`,
        pass: false,
      };
    }
  }
});

// Usage
test('should validate user object', () => {
  const user = { id: 1, name: 'John', email: 'john@example.com' };
  expect(user).toBeValidUser();
});

test('should check user permissions', () => {
  const user = { permissions: ['read', 'write'] };
  expect(user).toHavePermission('read');
});

Performance

Test Performance Optimization

// jest.config.js
module.exports = {
  // Parallel execution
  maxWorkers: '50%', // Use 50% of available cores

  // Cache
  cache: true,
  cacheDirectory: '/tmp/jest_cache',

  // Faster test discovery
  testPathIgnorePatterns: [
    '/node_modules/',
    '/build/',
    '/coverage/'
  ],

  // Optimize transforms
  transform: {
    '^.+\\.(js|jsx)$': ['babel-jest', { cacheDirectory: true }]
  },

  // Reduce overhead
  clearMocks: true,
  restoreMocks: true,

  // Faster test environment
  testEnvironment: 'jsdom',
  testEnvironmentOptions: {
    url: 'http://localhost'
  }
};

Selective Test Running

# Run only changed files
npm test -- --onlyChanged

# Run related tests
npm test -- --findRelatedTests src/components/Button.js

# Run tests matching pattern
npm test -- --testNamePattern="should render"

# Run specific test suites
npm test -- --testPathPattern="components"

# Bail on first failure
npm test -- --bail

# Run tests in band (no parallel)
npm test -- --runInBand

Memory Optimization

// jest.config.js
module.exports = {
  // Limit memory usage
  workerIdleMemoryLimit: '512MB',

  // Clear modules between tests
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true,

  // Optimize setup
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],

  // Reduce test overhead
  testEnvironment: 'jsdom',

  // Optimize coverage collection
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/**/*.test.{js,jsx}',
    '!src/**/*.stories.{js,jsx}'
  ]
};

// Cleanup in tests
afterEach(() => {
  jest.clearAllMocks();
  cleanup(); // From @testing-library/react
});

Integration

CI/CD Integration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test -- --ci --coverage --watchAll=false

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

Docker Integration

# Dockerfile.test
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "test", "--", "--ci", "--coverage", "--watchAll=false"]

IDE Integration

// .vscode/settings.json
{
  "jest.jestCommandLine": "npm test --",
  "jest.autoRun": {
    "watch": true,
    "onStartup": ["all-tests"]
  },
  "jest.showCoverageOnLoad": true,
  "jest.coverageFormatter": "DefaultFormatter",
  "jest.debugMode": true
}

Troubleshooting

Common Issues

# Clear Jest cache
npx jest --clearCache

# Debug Jest configuration
npx jest --showConfig

# Run with debug output
DEBUG=jest* npm test

# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm test

# Permission issues
sudo chown -R $(whoami) node_modules/.cache

Debugging Tests

// Debug specific test
test.only('debug this test', () => {
  console.log('Debug info');
  debugger; // Use with --inspect-brk
  expect(true).toBe(true);
});

// Debug with VS Code
// Add to launch.json
{
  "type": "node",
  "request": "launch",
  "name": "Jest Debug",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "--no-cache"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Mock Issues

// Reset mocks between tests
beforeEach(() => {
  jest.clearAllMocks();
  jest.resetAllMocks();
  jest.restoreAllMocks();
});

// Mock implementation issues
const mockFn = jest.fn();
mockFn.mockImplementation(() => {
  throw new Error('Mock error');
});

// Module mock issues
jest.mock('./module', () => ({
  __esModule: true,
  default: jest.fn(),
  namedExport: jest.fn()
}));

Best Practices

Test Organization

  • Descriptive Names: Use clear, descriptive test names
  • Arrange-Act-Assert: Follow the AAA pattern
  • Single Responsibility: One assertion per test when possible
  • Test Structure: Group related tests with describe blocks

Test Quality

  • Edge Cases: Test boundary conditions and edge cases
  • Error Handling: Test error scenarios and exceptions
  • Async Code: Properly test asynchronous operations
  • Mocking Strategy: Mock external dependencies appropriately

Performance

  • Selective Running: Use watch mode and selective test running
  • Parallel Execution: Leverage Jest's parallel execution
  • Cache Optimization: Use Jest's caching effectively
  • Memory Management: Clean up resources and mocks

Maintenance

  • Regular Updates: Keep Jest and testing libraries updated
  • Coverage Goals: Set and maintain appropriate coverage thresholds
  • Test Documentation: Document complex test scenarios
  • Refactoring: Refactor tests along with production code

Summary

Jest is a comprehensive JavaScript testing framework that provides:

  • Zero Configuration: Works out of the box for most projects
  • Powerful Matchers: Extensive assertion library with custom matcher support
  • Mocking Capabilities: Built-in mocking for functions, modules, and timers
  • Snapshot Testing: Visual regression testing for UI components
  • Code Coverage: Built-in coverage reporting with threshold enforcement
  • Parallel Execution: Fast test execution with worker processes
  • Watch Mode: Interactive development with automatic test re-running
  • Extensive Ecosystem: Rich ecosystem of plugins and integrations

Jest excels at testing React applications, Node.js backends, and general JavaScript code. Its focus on developer experience, comprehensive feature set, and excellent documentation make it the go-to choice for JavaScript testing in modern development workflows.