← All skills

Jest Skill

Unit testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill jest-skill

Advanced patterns

Advanced topics and patterns for experienced users.

Jest — Advanced Patterns & Playbook

Custom Matchers

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () => `expected ${received} to be within range ${floor} - ${ceiling}`
    };
  },
  toContainObject(received, argument) {
    const pass = this.equals(received, expect.arrayContaining([expect.objectContaining(argument)]));
    return { pass, message: () => `expected array to contain object ${JSON.stringify(argument)}` };
  }
});

test('value in range', () => expect(100).toBeWithinRange(90, 110));

Advanced Mocking Patterns

// Module factory mock with partial implementation
jest.mock('./api', () => ({
  ...jest.requireActual('./api'),
  fetchUser: jest.fn(),
  deleteUser: jest.fn()
}));

// Mock class with auto-mocked methods
jest.mock('./services/UserService');
const { UserService } = require('./services/UserService');
UserService.prototype.getUser.mockResolvedValue({ id: 1, name: 'Alice' });

// Spy on module method while keeping implementation
const utils = require('./utils');
jest.spyOn(utils, 'formatDate').mockReturnValue('2025-01-01');

// Manual mock with __mocks__ directory
// __mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: {} })),
  post: jest.fn(() => Promise.resolve({ data: {} })),
  create: jest.fn(function () { return this; }),
  interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }
};

// Timer mocking
jest.useFakeTimers();
test('debounced function', () => {
  const fn = jest.fn();
  const debounced = debounce(fn, 500);
  debounced(); debounced(); debounced();
  expect(fn).not.toHaveBeenCalled();
  jest.advanceTimersByTime(500);
  expect(fn).toHaveBeenCalledTimes(1);
});

// Mock date
jest.useFakeTimers({ now: new Date('2025-06-15T12:00:00Z') });
test('formats today', () => expect(getToday()).toBe('2025-06-15'));

Async Testing Patterns

// Async/await with error handling
test('rejects on network error', async () => {
  fetchUser.mockRejectedValueOnce(new Error('Network Error'));
  await expect(getUserProfile(1)).rejects.toThrow('Network Error');
});

// Testing streams/events
test('emits data events', (done) => {
  const stream = createReadStream('test.txt');
  const chunks = [];
  stream.on('data', chunk => chunks.push(chunk));
  stream.on('end', () => { expect(chunks.length).toBeGreaterThan(0); done(); });
});

// Retry logic testing
test('retries failed requests', async () => {
  fetchUser
    .mockRejectedValueOnce(new Error('timeout'))
    .mockRejectedValueOnce(new Error('timeout'))
    .mockResolvedValueOnce({ id: 1 });
  const result = await fetchWithRetry(fetchUser, 3);
  expect(result).toEqual({ id: 1 });
  expect(fetchUser).toHaveBeenCalledTimes(3);
});

// waitFor pattern with polling
const waitFor = (fn, { timeout = 5000, interval = 50 } = {}) =>
  new Promise((resolve, reject) => {
    const start = Date.now();
    const check = async () => {
      try { resolve(await fn()); }
      catch (e) {
        if (Date.now() - start >= timeout) reject(e);
        else setTimeout(check, interval);
      }
    };
    check();
  });

Snapshot Testing

// Inline snapshots (auto-filled by Jest)
test('user object shape', () => {
  expect(createUser('Alice')).toMatchInlineSnapshot(`
    { "id": Any<String>, "name": "Alice", "createdAt": Any<Date> }
  `);
});

// Property matchers for dynamic values
test('user with dynamic fields', () => {
  expect(createUser('Bob')).toMatchSnapshot({
    id: expect.any(String),
    createdAt: expect.any(Date)
  });
});

// Serializer for custom types
expect.addSnapshotSerializer({
  test: val => val && val.type === 'Component',
  serialize: (val) => `<${val.name} props={${JSON.stringify(val.props)}} />`
});

React Testing with Testing Library

import { render, screen, waitFor, within, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Complete component test
describe('TodoList', () => {
  const user = userEvent.setup();

  it('adds and removes todo items', async () => {
    render(<TodoList />);
    const input = screen.getByPlaceholderText('Add a todo');
    await user.type(input, 'Buy milk');
    await user.click(screen.getByRole('button', { name: /add/i }));
    expect(screen.getByText('Buy milk')).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /delete/i }));
    expect(screen.queryByText('Buy milk')).not.toBeInTheDocument();
  });

  it('shows loading state', async () => {
    render(<TodoList />);
    expect(screen.getByRole('progressbar')).toBeInTheDocument();
    await waitFor(() => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument());
  });

  it('handles form validation', async () => {
    render(<TodoList />);
    await user.click(screen.getByRole('button', { name: /add/i }));
    expect(screen.getByRole('alert')).toHaveTextContent('Todo cannot be empty');
  });
});

// Context/Provider testing
const renderWithProviders = (ui, { initialState, ...options } = {}) => {
  const store = createStore(initialState);
  const Wrapper = ({ children }) => (
    <ThemeProvider><StoreProvider store={store}>{children}</StoreProvider></ThemeProvider>
  );
  return { ...render(ui, { wrapper: Wrapper, ...options }), store };
};

// Hook testing
import { renderHook, act } from '@testing-library/react';
test('useCounter', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

Performance Testing with Jest

test('processes 10k items under 100ms', () => {
  const items = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
  const start = performance.now();
  processItems(items);
  expect(performance.now() - start).toBeLessThan(100);
});

Configuration Best Practices

// jest.config.js — production-grade
module.exports = {
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/src'],
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/index.{js,ts}'],
  coverageThresholds: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|svg)$': '<rootDir>/__mocks__/fileMock.js'
  },
  setupFilesAfterSetup: ['<rootDir>/jest.setup.js'],
  transform: { '^.+\\.(ts|tsx)$': 'ts-jest' },
  testMatch: ['**/__tests__/**/*.(spec|test).[jt]s?(x)'],
  watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname']
};

Anti-Patterns

  • test('works', () => { myFunction(); }) — assertion-free tests prove nothing
  • ❌ Testing implementation details (internal state, private methods)
  • ❌ Snapshot overuse — large snapshots become meaningless; prefer targeted assertions
  • jest.mock() at file level when only one test needs it — use jest.spyOn per-test instead
  • setTimeout in tests — use jest.useFakeTimers() and advanceTimersByTime
  • ❌ Shared mutable state between tests — always reset in beforeEach