A

Jest - The Established Standard

testingjestunit-testingtypescriptjavascript

Jest - The Established Standard

Jest is Facebook's JavaScript testing framework that became the de facto standard for testing React applications and Node.js projects. It provides a complete testing solution out of the box: test runner, assertion library, mocking utilities, and code coverage. While Vitest is gaining ground for Vite-based projects, Jest remains the right choice for many existing codebases and specific ecosystem needs.

When to Use Jest vs. Vitest

Choose Jest when:

  • You have an existing Jest test suite (migration has costs)
  • You're using Create React App, Next.js, or other Jest-preconfigured setups
  • You need mature snapshot testing with established workflows
  • Your team knows Jest and values stability over cutting-edge features
  • You're testing Node.js backend code without a Vite build

Choose Vitest when:

  • You're starting a new Vite-based project
  • You need native ESM support without configuration headaches
  • You want faster test execution with Vite's HMR
  • You're already using Vite and want unified configuration

Installation and Setup

pnpm add -D jest @types/jest

For TypeScript projects, you'll need a transformer. The three main options:

Option 1: ts-jest (Most Compatible)

pnpm add -D ts-jest
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

Option 2: @swc/jest (Fastest)

pnpm add -D @swc/jest @swc/core
// jest.config.js
module.exports = {
  transform: {
    '^.+\\.(t|j)sx?$': '@swc/jest',
  },
  testEnvironment: 'node',
};

Option 3: babel-jest (Most Flexible)

pnpm add -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
};

// jest.config.js
module.exports = {
  testEnvironment: 'node',
};

Recommendation: Use @swc/jest for speed in most projects. Use ts-jest if you need full TypeScript type-checking during tests or have complex TypeScript features.

Add Test Scripts

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

Configuration Deep Dive

Create jest.config.js (or jest.config.ts with ts-node) in your project root:

// jest.config.js
/** @type {import('jest').Config} */
module.exports = {
  // TypeScript transformation
  preset: 'ts-jest',

  // Test environment
  testEnvironment: 'jsdom', // or 'node' for backend

  // Where to find tests
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],

  // Module resolution (match your tsconfig paths)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    // Handle CSS imports in tests
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    // Handle image imports
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
  },

  // Setup files
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],

  // Coverage configuration
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/test/**',
    '!src/**/index.ts', // barrel files
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },

  // Performance
  maxWorkers: '50%',

  // Clear mocks between tests
  clearMocks: true,
};

Essential Configuration Options

Option Description Common Values
testEnvironment DOM or Node.js 'jsdom', 'node'
roots Where to look for tests ['<rootDir>/src']
testMatch File patterns ['**/*.test.ts']
moduleNameMapper Path aliases, asset mocks See above
setupFilesAfterEnv Run before each test file ['./src/test/setup.ts']
transform Code transformers { '^.+\\.tsx?$': 'ts-jest' }
maxWorkers Parallelism '50%', 4, 1
testTimeout Default timeout (ms) 5000

Core API

Test Structure

// src/utils/math.test.ts
import { add, divide } from './math';

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

    it('handles negative numbers', () => {
      expect(add(-1, 1)).toBe(0);
    });

    test('is an alias for it', () => {
      // test() and it() are identical
      expect(add(0, 0)).toBe(0);
    });
  });

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

    it('throws on division by zero', () => {
      expect(() => divide(1, 0)).toThrow('Cannot divide by zero');
    });
  });
});

Matchers

// Equality
expect(value).toBe(5);                    // Strict equality (Object.is)
expect(obj).toEqual({ a: 1 });            // Deep equality
expect(obj).toStrictEqual({ a: 1 });      // Deep equality + prototype check

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

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(5);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);    // 5 decimal places

// Strings
expect(str).toMatch(/pattern/);
expect(str).toContain('substring');
expect(str).toHaveLength(5);

// Arrays
expect(arr).toContain(item);
expect(arr).toContainEqual({ id: 1 });    // Deep equality for items
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([1, 2]));

// Objects
expect(obj).toHaveProperty('key');
expect(obj).toHaveProperty('nested.key', 'value');
expect(obj).toMatchObject({ partial: 'match' });
expect(obj).toEqual(expect.objectContaining({ key: 'value' }));

// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('message');
expect(() => fn()).toThrow(Error);
expect(() => fn()).toThrow(/pattern/);

// Negation
expect(value).not.toBe(5);
expect(arr).not.toContain(item);

Async Testing

// Async/await (recommended)
it('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('John');
});

// Promises
it('fetches user data', () => {
  return fetchUser(1).then(user => {
    expect(user.name).toBe('John');
  });
});

// Resolves/rejects matchers
it('resolves with user', async () => {
  await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'John' });
});

it('rejects for invalid id', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Not found');
});

// Callbacks (legacy)
it('calls callback with data', done => {
  fetchUserCallback(1, (err, user) => {
    expect(err).toBeNull();
    expect(user.name).toBe('John');
    done();
  });
});

Setup and Teardown

describe('database operations', () => {
  // Run once before all tests in this describe
  beforeAll(async () => {
    await db.connect();
  });

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

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

  // Run after each test
  afterEach(async () => {
    await db.rollback();
  });

  it('creates user', async () => {
    const user = await db.createUser({ name: 'John' });
    expect(user.id).toBeDefined();
  });
});

Setup functions can be nested and run in order: outer beforeAll → outer beforeEach → inner beforeEach → test → inner afterEach → outer afterEach.

Mocking

Function Mocks with jest.fn()

// Basic mock function
const mockFn = jest.fn();
mockFn('arg1', 'arg2');

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg1', 'arg2');

// Return values
const mockFn = jest.fn()
  .mockReturnValue('default')
  .mockReturnValueOnce('first')
  .mockReturnValueOnce('second');

mockFn(); // 'first'
mockFn(); // 'second'
mockFn(); // 'default'

// Async return values
const mockAsync = jest.fn()
  .mockResolvedValue({ data: 'success' })
  .mockResolvedValueOnce({ data: 'first' })
  .mockRejectedValueOnce(new Error('fail'));

// Custom implementation
const mockFn = jest.fn((x: number) => x * 2);
expect(mockFn(5)).toBe(10);

Module Mocking with jest.mock()

// src/services/userService.ts
import { api } from './api';

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

// src/services/userService.test.ts
import { getUser } from './userService';
import { api } from './api';

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

// TypeScript: cast to mocked type
const mockedApi = jest.mocked(api);

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

  it('fetches user from API', async () => {
    mockedApi.get.mockResolvedValue({ data: { id: 1, name: 'John' } });

    const user = await getUser(1);

    expect(mockedApi.get).toHaveBeenCalledWith('/users/1');
    expect(user).toEqual({ id: 1, name: 'John' });
  });
});

Factory Mocking

Provide custom implementations:

jest.mock('./api', () => ({
  api: {
    get: jest.fn(),
    post: jest.fn(),
  },
}));

// Access original module
jest.mock('./utils', () => ({
  ...jest.requireActual('./utils'),
  helperFunction: jest.fn(),
}));

Manual Mocks with __mocks__

Create manual mocks in a __mocks__ directory adjacent to the module:

src/
  services/
    api.ts
    __mocks__/
      api.ts
// src/services/__mocks__/api.ts
export const api = {
  get: jest.fn().mockResolvedValue({ data: {} }),
  post: jest.fn().mockResolvedValue({ data: {} }),
};

Then in your test:

jest.mock('./api'); // Automatically uses __mocks__/api.ts

Spying with jest.spyOn()

const user = {
  getName() {
    return 'John';
  },
};

// Spy without changing implementation
const spy = jest.spyOn(user, 'getName');
user.getName();
expect(spy).toHaveBeenCalled();

// Spy with mock implementation
jest.spyOn(user, 'getName').mockReturnValue('Jane');
expect(user.getName()).toBe('Jane');

// Spy on prototype methods
jest.spyOn(Array.prototype, 'push');

// Restore original
spy.mockRestore();

Timer Mocks

beforeEach(() => {
  jest.useFakeTimers();
});

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

it('debounces function calls', () => {
  const fn = jest.fn();
  const debounced = debounce(fn, 1000);

  debounced();
  expect(fn).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1000);
  expect(fn).toHaveBeenCalledTimes(1);
});

// Other timer methods
jest.runAllTimers();           // Run all pending timers
jest.runOnlyPendingTimers();   // Run pending, not newly scheduled
jest.advanceTimersToNextTimer(); // Advance to next timer
jest.setSystemTime(new Date('2024-01-01')); // Mock Date.now()

Snapshot Testing

Snapshots capture output and compare against stored versions:

// Component snapshot
it('renders correctly', () => {
  const tree = renderer.create(<Button label="Click me" />).toJSON();
  expect(tree).toMatchSnapshot();
});

// Data snapshot
it('returns expected structure', () => {
  const config = generateConfig({ env: 'production' });
  expect(config).toMatchSnapshot();
});

// Inline snapshot (stored in test file)
it('formats date correctly', () => {
  expect(formatDate(new Date('2024-01-15'))).toMatchInlineSnapshot(
    `"January 15, 2024"`
  );
});

Update snapshots when intentional changes occur:

pnpm test -- -u

See Snapshot Testing for detailed guidance on when and how to use snapshots effectively.

Performance Optimization

Running Tests in Band

For CI or when tests conflict:

jest --runInBand  # Run serially, one at a time

Controlling Workers

jest --maxWorkers=4     # Fixed number
jest --maxWorkers=50%   # Percentage of CPUs

Test Sharding

Split tests across CI jobs:

# Job 1
jest --shard=1/3

# Job 2
jest --shard=2/3

# Job 3
jest --shard=3/3

Speeding Up TypeScript

If ts-jest is slow, switch to @swc/jest:

// jest.config.js
module.exports = {
  transform: {
    '^.+\\.(t|j)sx?$': ['@swc/jest', {
      jsc: {
        transform: {
          react: {
            runtime: 'automatic',
          },
        },
      },
    }],
  },
};

Caching

Jest caches transformed files. Clear cache if you have stale results:

jest --clearCache

Running Tests

Basic Commands

# Run all tests
pnpm test

# Watch mode
pnpm test -- --watch

# Watch only changed files
pnpm test -- --watchAll=false --onlyChanged

# Run specific file
pnpm test -- src/utils/math.test.ts

# Run tests matching pattern
pnpm test -- --testNamePattern="creates user"

# Run tests in specific directory
pnpm test -- --testPathPattern="src/components"

# Verbose output
pnpm test -- --verbose

Filtering in Code

// Skip tests
describe.skip('broken feature', () => {});
it.skip('incomplete test', () => {});

// Focus tests (only these run)
describe.only('feature I am debugging', () => {});
it.only('just this test', () => {});

// Conditional
const isCI = process.env.CI === 'true';
(isCI ? it.skip : it)('skipped in CI', () => {});

Code Coverage

pnpm test -- --coverage

Coverage configuration in jest.config.js:

module.exports = {
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/test/**',
  ],
  coverageReporters: ['text', 'lcov', 'html'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    // Stricter for specific paths
    './src/utils/': {
      branches: 95,
      functions: 95,
      lines: 95,
      statements: 95,
    },
  },
};

Testing React Components

Install Testing Library:

pnpm add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event

Setup file:

// src/test/setup.ts
import '@testing-library/jest-dom';
// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  testEnvironment: 'jsdom',
};

Example component test:

// src/components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  it('increments count on click', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    expect(screen.getByText('Count: 0')).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /increment/i }));

    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

Gotchas and Tips

ESM Support

Jest's ESM support is experimental. For projects using ES modules:

// jest.config.js
module.exports = {
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { useESM: true }],
  },
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
};

Or switch to Vitest for native ESM.

Mocking Node Modules

Mock packages in node_modules by creating __mocks__ at project root:

__mocks__/
  axios.ts
src/
  ...
// __mocks__/axios.ts
export default {
  get: jest.fn().mockResolvedValue({ data: {} }),
  post: jest.fn().mockResolvedValue({ data: {} }),
};

Debugging Tests

# Run with Node debugger
node --inspect-brk node_modules/.bin/jest --runInBand

# Or in VS Code, add launch config:
{
  "type": "node",
  "request": "launch",
  "name": "Debug Jest",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "--no-cache"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Common Errors

"Cannot use import statement outside a module"

  • Your transformer isn't processing the file. Check transform config.
  • The file might be in node_modules. Add to transformIgnorePatterns.

"Jest encountered an unexpected token"

  • Similar cause. Ensure the file extension is being transformed.

Tests pass individually but fail together

  • Shared mutable state. Use beforeEach to reset state.
  • Mock not cleared. Add clearMocks: true to config or call jest.clearAllMocks().

See Also

Last updated: March 23, 2026