jest

SKILL.md

Jest — JavaScript Testing Framework

Jest brings a batteries-included approach to JavaScript testing. Where other frameworks require you to assemble a test runner, assertion library, and mocking tool separately, Jest ships all three in a single package. You install it, write a test, and run it. That simplicity is why it dominates the JavaScript testing landscape.

This skill walks you through Jest from first principles — writing assertions, mocking dependencies, testing asynchronous code, generating coverage reports, and integrating everything into a CI pipeline.

Installing and Configuring Jest

Every Jest setup begins with installation and a configuration file. Jest works out of the box for plain JavaScript, but most real projects need a bit of configuration for TypeScript, JSX, or module resolution.

# Install Jest and its TypeScript support
npm install --save-dev jest ts-jest @types/jest

Once installed, create a configuration file at the root of your project. The jest.config.ts format gives you type checking on your configuration options.

// jest.config.ts — Root Jest configuration for a TypeScript project
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.test.ts', '**/*.spec.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/index.ts',
  ],
  coverageThresholds: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

export default config;

Add test scripts to your package.json so your team has consistent commands.

// package.json — Scripts section for Jest commands
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit"
  }
}

Writing Assertions

Jest's expect API gives you a fluent interface for asserting values. Every test follows the same pattern: arrange your data, act on it, then assert the result.

// src/__tests__/math.test.ts — Basic assertion patterns with Jest matchers
describe('arithmetic operations', () => {
  test('adds two numbers correctly', () => {
    const result = add(2, 3);
    expect(result).toBe(5);
  });

  test('returns an object with computed properties', () => {
    const result = createUser('Alice', 30);

    // toEqual performs deep equality, unlike toBe which checks reference
    expect(result).toEqual({
      name: 'Alice',
      age: 30,
      id: expect.any(String),
    });
  });

  test('array contains specific items', () => {
    const fruits = getFruits();

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

  test('function throws on invalid input', () => {
    expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    expect(() => divide(10, 0)).toThrow(ArithmeticError);
  });
});

Mock Functions and Module Mocking

Mocking is where Jest truly shines. You can replace any function, module, or timer with a controllable substitute. This lets you test units in complete isolation.

// src/__tests__/userService.test.ts — Mocking external dependencies
import { UserService } from '../userService';
import { database } from '../database';

// Replace the entire database module with auto-mocked version
jest.mock('../database');

const mockedDb = jest.mocked(database);

describe('UserService', () => {
  beforeEach(() => {
    // Clear all mock state between tests
    jest.clearAllMocks();
  });

  test('fetches a user by ID from the database', async () => {
    const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' };
    mockedDb.findById.mockResolvedValue(mockUser);

    const service = new UserService();
    const user = await service.getUser('1');

    expect(mockedDb.findById).toHaveBeenCalledWith('1');
    expect(mockedDb.findById).toHaveBeenCalledTimes(1);
    expect(user).toEqual(mockUser);
  });

  test('throws when user is not found', async () => {
    mockedDb.findById.mockResolvedValue(null);

    const service = new UserService();

    await expect(service.getUser('999')).rejects.toThrow('User not found');
  });
});

For more granular control, jest.fn() creates standalone mock functions you can pass as callbacks or method implementations.

// src/__tests__/eventHandler.test.ts — Using jest.fn() for callback testing
describe('event handler', () => {
  test('calls the callback with processed data', () => {
    const callback = jest.fn();

    processEvents([{ type: 'click', target: 'button' }], callback);

    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenCalledWith({
      type: 'click',
      target: 'button',
      timestamp: expect.any(Number),
    });
  });

  test('mock implementation controls return value', () => {
    const getPrice = jest.fn()
      .mockReturnValueOnce(10.99)
      .mockReturnValueOnce(24.99)
      .mockReturnValue(0);

    expect(getPrice()).toBe(10.99);
    expect(getPrice()).toBe(24.99);
    expect(getPrice()).toBe(0);
  });
});

Testing Asynchronous Code

Modern JavaScript is heavily asynchronous. Jest handles promises, async/await, and callbacks with equal ease. The key is always returning or awaiting the asynchronous operation so Jest knows when the test is done.

// src/__tests__/api.test.ts — Patterns for testing async operations
describe('API client', () => {
  test('fetches data with async/await', async () => {
    const data = await fetchUserProfile('alice');

    expect(data.username).toBe('alice');
    expect(data.posts).toBeInstanceOf(Array);
  });

  test('handles API errors gracefully', async () => {
    // Mock fetch to simulate a network failure
    global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));

    const result = await fetchWithRetry('/api/data', { retries: 3 });

    expect(result.error).toBe('Network error');
    expect(global.fetch).toHaveBeenCalledTimes(4); // initial + 3 retries
  });

  test('resolves multiple concurrent requests', async () => {
    const [users, posts] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
    ]);

    expect(users).toHaveLength(10);
    expect(posts).toHaveLength(25);
  });
});

Snapshot Testing

Snapshots capture the output of a component or function and compare it against a saved reference. They are invaluable for catching unintended changes in UI components or serialized data structures.

// src/__tests__/components.test.tsx — Snapshot testing for React components
import { render } from '@testing-library/react';
import { UserCard } from '../components/UserCard';

describe('UserCard', () => {
  test('renders correctly with user data', () => {
    const { container } = render(
      <UserCard
        name="Alice Johnson"
        email="alice@example.com"
        role="admin"
      />
    );

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

  test('inline snapshot for small outputs', () => {
    const formatted = formatAddress({
      street: '123 Main St',
      city: 'Springfield',
      state: 'IL',
    });

    expect(formatted).toMatchInlineSnapshot(`"123 Main St, Springfield, IL"`);
  });
});

When a snapshot test fails because you intentionally changed the output, update the snapshots with jest --updateSnapshot.

Coverage Reports and Watch Mode

Jest's built-in coverage tool uses Istanbul under the hood. It generates reports showing which lines, branches, functions, and statements your tests exercise.

# Generate a coverage report in multiple formats
npx jest --coverage --coverageReporters='text' --coverageReporters='lcov'

Watch mode is where Jest becomes a development companion. It watches for file changes and re-runs only the tests affected by those changes.

# Start watch mode — press 'p' to filter by filename, 't' to filter by test name
npx jest --watch

Watch mode supports interactive filtering. Press p to filter tests by a filename regex, t to filter by test name, or a to run all tests. This tight feedback loop makes TDD practical even in large codebases.

CI Integration

In continuous integration, Jest should run with specific flags that optimize for non-interactive environments and produce machine-readable output.

# .github/workflows/test.yml — GitHub Actions workflow running Jest
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npx jest --ci --coverage --maxWorkers=2

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/lcov-report/

The --ci flag changes snapshot behavior to fail instead of writing new snapshots, preventing accidental snapshot updates in CI. The --maxWorkers flag controls parallelism to match your CI runner's CPU count and avoid out-of-memory failures.

Weekly Installs
1
GitHub Stars
15
First Seen
3 days ago
Installed on
amp1
cline1
augment1
opencode1
cursor1
kimi-cli1