All topics
Testing · Learning hub

Jest notes for developers

Master Jest with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Testing notes
Jest

Test Basics & Matchers

Test Basics & Matchers Structure // Organizing tests describe('UserService', () => { describe('create', () => { it('should create a user with valid data', () =>

Test Basics & Matchers

Structure

// Organizing tests
describe('UserService', () => {
  describe('create', () => {
    it('should create a user with valid data', () => { ... });
    it('should throw when email is invalid', () => { ... });
  });

  describe('delete', () => {
    test('removes the user from db', () => { ... });
  });
});

// Setup and teardown
beforeAll(async () => {
  await db.connect();       // runs once before all tests in file
});

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

beforeEach(() => {
  jest.clearAllMocks();     // before each test in the block
});

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

Matchers

// Equality
expect(value).toBe(42);             // strict equality (Object.is)
expect(obj).toEqual({ a: 1 });      // deep equality
expect(obj).toStrictEqual({ a: 1 }); // deep equality + type check (undefined vs missing)
expect(obj).not.toBe(other);

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

// Numbers
expect(n).toBeGreaterThan(5);
expect(n).toBeGreaterThanOrEqual(5);
expect(n).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);  // floating point

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

// Arrays
expect(arr).toContain(42);
expect(arr).toContainEqual({ id: 1 });  // deep
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([1, 2]));  // subset

// Objects
expect(obj).toHaveProperty('user.name');
expect(obj).toHaveProperty('count', 5);
expect(obj).toMatchObject({ status: 'ok' });  // partial match

// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('error message');
expect(() => fn()).toThrow(TypeError);
await expect(asyncFn()).rejects.toThrow('error');
await expect(asyncFn()).rejects.toMatchObject({ statusCode: 404 });

// Asymmetric matchers
expect(user).toEqual({
  id: expect.any(Number),
  email: expect.stringContaining('@'),
  roles: expect.arrayContaining(['user']),
  createdAt: expect.any(Date),
});

Async Tests

// Async/await — preferred
it('should fetch user', async () => {
  const user = await getUser(1);
  expect(user.name).toBe('Alice');
});

// Promises
it('should fetch user', () => {
  return getUser(1).then(user => {
    expect(user.name).toBe('Alice');
  });
});

// resolves/rejects matchers
it('should resolve', async () => {
  await expect(getUser(1)).resolves.toMatchObject({ name: 'Alice' });
});

it('should reject', async () => {
  await expect(getUser(-1)).rejects.toThrow('Not found');
});

// Timeout override
it('slow test', async () => {
  await slowOperation();
}, 10000);  // 10 second timeout

Test Configuration

// jest.config.ts
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',           // or 'jsdom' for browser
  setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
  collectCoverage: true,
  coverageThreshold: {
    global: { branches: 70, functions: 80, lines: 80, statements: 80 }
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',  // path aliases
  },
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
  testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
};

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

Mocking & Advanced

Mocking & Advanced jest.fn() — Mock Functions const mockFn = jest.fn(); // Set return value mockFn.mockReturnValue(42); mockFn.mockReturnValueOnce(10).mockRetur

Mocking & Advanced

jest.fn() — Mock Functions

const mockFn = jest.fn();

// Set return value
mockFn.mockReturnValue(42);
mockFn.mockReturnValueOnce(10).mockReturnValueOnce(20);

// Async
mockFn.mockResolvedValue({ id: 1, name: 'Alice' });
mockFn.mockRejectedValue(new Error('Failed'));

// Implementation
mockFn.mockImplementation((x: number) => x * 2);
mockFn.mockImplementationOnce(() => 'first call');

// Assertions on mock
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledWith('arg1', 42);
expect(mockFn).toHaveBeenLastCalledWith('last-arg');
expect(mockFn).toHaveBeenNthCalledWith(2, 'second-call-arg');

// Access calls
console.log(mockFn.mock.calls);        // [[arg1, arg2], [arg3], ...]
console.log(mockFn.mock.results);      // [{type: 'return', value: 42}, ...]
console.log(mockFn.mock.instances);    // if called as constructor

// Reset / clear
mockFn.mockClear();     // clear calls, instances, results (keep implementation)
mockFn.mockReset();     // clear + remove return values/implementations
mockFn.mockRestore();   // restore original (only for jest.spyOn)

jest.mock() — Module Mocking

// Auto-mock entire module
jest.mock('../services/emailService');
import { sendEmail } from '../services/emailService';
const mockedSendEmail = sendEmail as jest.MockedFunction<typeof sendEmail>;
mockedSendEmail.mockResolvedValue(undefined);

// Manual mock with factory
jest.mock('../db', () => ({
  findUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
  createUser: jest.fn().mockResolvedValue({ id: 2 }),
}));

// Partial mock — keep real implementations for some exports
jest.mock('../utils', () => ({
  ...jest.requireActual('../utils'),  // keep real implementations
  generateId: jest.fn().mockReturnValue('test-id'),
}));

// Mock default export
jest.mock('../config', () => ({
  default: { apiUrl: 'http://test-api', timeout: 1000 },
}));

// ES module default export
jest.mock('../logger', () => ({
  __esModule: true,
  default: {
    info: jest.fn(),
    error: jest.fn(),
  },
}));

jest.spyOn() — Spy on Methods

// Spy on existing method (can still call original)
const spy = jest.spyOn(userService, 'findById');
spy.mockResolvedValue({ id: 1, name: 'Alice' });

expect(spy).toHaveBeenCalledWith(1);
spy.mockRestore();  // restore original implementation

// Spy without changing behavior
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

// Spy on property
jest.spyOn(Date, 'now').mockReturnValue(1700000000000);

// Mock timers
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));

setTimeout(() => callback(), 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();

jest.useRealTimers();

Testing React Components

// @testing-library/react
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('renders user name', () => {
    render(<UserCard user={{ id: 1, name: 'Alice', email: 'a@b.com' }} />);
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByRole('heading', { name: 'Alice' })).toBeInTheDocument();
  });

  it('calls onDelete when button clicked', async () => {
    const onDelete = jest.fn();
    const user = userEvent.setup();
    render(<UserCard user={{ id: 1, name: 'Alice' }} onDelete={onDelete} />);

    await user.click(screen.getByRole('button', { name: /delete/i }));
    expect(onDelete).toHaveBeenCalledWith(1);
  });

  it('shows loading state', async () => {
    render(<UserCard userId={1} />);
    expect(screen.getByRole('progressbar')).toBeInTheDocument();
    await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument());
  });

  // Prefer queries: getBy (throws), queryBy (null), findBy (async)
  // getByRole > getByLabelText > getByText > getByTestId (last resort)
});

Snapshot Testing

// Snapshot — save rendered output, fail when it changes
it('matches snapshot', () => {
  const { container } = render(<Button label="Click me" />);
  expect(container).toMatchSnapshot();
});

// Inline snapshot
expect(user).toMatchInlineSnapshot(`
  Object {
    "email": "alice@example.com",
    "id": 1,
    "name": "Alice",
  }
`);

// Update snapshots: jest --updateSnapshot (or jest -u)
// Use sparingly — snapshots of large components are brittle
Jest

Interview Questions

Jest Interview Questions Q: What is the difference between unit, integration, and e2e tests? Unit — test a single function/class in isolation; all dependencies

Jest Interview Questions

Q: What is the difference between unit, integration, and e2e tests?

  • Unit — test a single function/class in isolation; all dependencies are mocked. Fast, numerous, pinpoint failures.

  • Integration — test multiple modules together (e.g., service + real database). Slower, fewer, catch connection issues.

  • E2E — test the full system from user perspective (browser automation). Slowest, fewest, highest confidence.

Q: What is the difference between toBe and toEqual?

toBe uses Object.is (strict reference equality) — passes for primitives and identical object references. toEqual performs deep equality check — two different objects with the same structure will pass. Use toBe for primitives and same-reference checks, toEqual for objects and arrays.

Q: What is the difference between mockClear, mockReset, and mockRestore?

  • mockClear — resets mock.calls, mock.instances, mock.results. Keeps mock implementation and return values.

  • mockReset — everything mockClear does + removes mock implementation and return values.

  • mockRestore — everything mockReset does + restores the original (non-mocked) implementation. Only works on jest.spyOn() mocks.

Q: How do you test that a function throws?

// Sync
expect(() => fn(badInput)).toThrow('error message');
expect(() => fn(badInput)).toThrow(TypeError);

// Async
await expect(asyncFn(badInput)).rejects.toThrow('error message');
await expect(asyncFn(badInput)).rejects.toBeInstanceOf(NotFoundError);

Q: What is code coverage and what should you aim for?

Coverage measures what percentage of your code is executed during tests: statements, branches, functions, lines. 100% is usually impractical and not always meaningful — focus on critical business logic. A practical target is 70-80% line coverage with high coverage on core paths. Use coverage to find untested areas, not as a quality metric in itself.

Q: When should you use snapshot tests?

Snapshot tests are useful for stable UI components where you want to detect unintended visual regressions. They are a poor substitute for behavioral tests — a snapshot tells you what changed but not if the change is correct. Avoid large component snapshots (too brittle), update snapshots via --updateSnapshot after intentional changes.

Keep your Jest knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever