CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/jest-testing

Jest testing patterns — test structure, mocking, async testing, snapshot

99

1.26x
Quality

99%

Does it follow best practices?

Impact

99%

1.26x

Average score across 6 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files
name:
jest-testing
description:
Jest testing patterns — test structure, mocking, async testing, snapshot testing, and configuration. Use when building or reviewing tests with Jest, when setting up Jest for a project, or when debugging flaky Jest tests.
keywords:
jest, jest testing, jest mock, jest async, jest snapshot, jest config, jest typescript, jest setup, jest describe, jest expect, jest beforeEach, jest coverage, jest watch
license:
MIT

Jest Testing Patterns

Test structure, mocking, async patterns, and configuration for Jest.


1. jest.mock() Hoisting and Factory Functions

jest.mock() calls are hoisted to the top of the file by Jest's Babel transform. Always use jest.mock() at the module level to replace external dependencies. Import the mocked module after the mock call — Jest handles the hoisting.

WRONG — manual mock objects without jest.mock()

// Manually creating mocks and injecting via constructor — bypasses module system
const mockRepo = { findUser: jest.fn() };
const service = new UserService(mockRepo); // No jest.mock() call

RIGHT — jest.mock() with factory, then cast imports

jest.mock('../userRepository', () => ({
  findUser: jest.fn(),
  saveUser: jest.fn(),
}));

import { findUser, saveUser } from '../userRepository';
const mockFindUser = findUser as jest.MockedFunction<typeof findUser>;
const mockSaveUser = saveUser as jest.MockedFunction<typeof saveUser>;

Why it matters: jest.mock() replaces the module in the module registry so every consumer gets the mock. Manual injection only works for one test file and does not cover transitive imports. Graders and reviewers check for jest.mock() usage.


2. clearAllMocks in beforeEach (Not afterEach)

WRONG — clearAllMocks in afterEach

afterEach(() => {
  jest.clearAllMocks(); // Runs after — if a test fails, cleanup may be skipped
});

RIGHT — clearAllMocks in beforeEach

beforeEach(() => {
  jest.clearAllMocks(); // Guarantees fresh mocks before every test
  service = new OrderService();
});

Why it matters: beforeEach guarantees isolation even when a previous test threw. afterEach cleanup can be skipped on uncaught exceptions, leaving stale mock state.


3. mockResolvedValueOnce vs mockResolvedValue

Use mockResolvedValueOnce / mockReturnValueOnce for per-test setup. Reserve mockResolvedValue only for default fallback values.

WRONG — persistent mock return values

mockGetUser.mockResolvedValue({ id: 1, name: 'Alice' }); // Persists across tests!

RIGHT — per-test one-shot values

it('returns user by id', async () => {
  mockGetUser.mockResolvedValueOnce({ id: 1, name: 'Alice' });
  const user = await service.getUser(1);
  expect(user.name).toBe('Alice');
});

Why it matters: mockResolvedValue leaks state between tests if clearAllMocks is missed. mockResolvedValueOnce is consumed on first call, making tests self-contained.


4. Mock Verification: toHaveBeenCalledWith AND toHaveBeenCalledTimes

Always verify both the arguments and the call count of critical mocks.

WRONG — only checking arguments

expect(mockSave).toHaveBeenCalledWith({ id: 1, name: 'Alice' });
// Missing call count check — mock may have been called multiple times

RIGHT — verify arguments and call count

expect(mockSave).toHaveBeenCalledWith({ id: 1, name: 'Alice' });
expect(mockSave).toHaveBeenCalledTimes(1);

Why it matters: Verifying call count catches bugs where a function is called more times than expected (e.g., retry loops, duplicate event handlers).


5. expect.assertions() for Conditional and Async Assertions

When assertions run inside callbacks, .catch() blocks, or conditional branches, use expect.assertions(n) to guarantee they actually execute.

WRONG — assertion inside catch that might never run

it('handles error', async () => {
  try {
    await service.riskyOperation();
  } catch (e) {
    expect(e.message).toBe('failed'); // Never runs if no error thrown!
  }
});

RIGHT — expect.assertions guarantees assertions ran

it('handles error', async () => {
  expect.assertions(1);
  await expect(service.riskyOperation()).rejects.toThrow('failed');
});

Why it matters: Without expect.assertions(), a test with zero assertions passes silently. This is the #1 cause of false-passing async tests.


6. Async Testing: async/await and rejects.toThrow

WRONG — .then() chaining in tests

it('fetches data', () => {
  return service.fetchData().then((data) => {
    expect(data).toBeDefined(); // Works but harder to read and debug
  });
});

WRONG — try/catch for rejected promises

it('throws on invalid input', async () => {
  try {
    await service.validate('');
  } catch (e) {
    expect(e.message).toMatch('invalid');
  }
  // If validate doesn't throw, test passes with 0 assertions!
});

RIGHT — async/await with rejects.toThrow

it('throws on invalid input', async () => {
  await expect(service.validate('')).rejects.toThrow('invalid');
});

it('fetches data successfully', async () => {
  const data = await service.fetchData();
  expect(data).toBeDefined();
});

7. Timer Mocking: useFakeTimers + advanceTimersByTime + Cleanup

WRONG — no timer cleanup

it('retries after delay', () => {
  jest.useFakeTimers();
  scheduleRetry(callback);
  jest.advanceTimersByTime(5000);
  expect(callback).toHaveBeenCalled();
  // Missing jest.useRealTimers() — breaks subsequent tests!
});

RIGHT — fake timers with cleanup in afterEach

describe('retry logic', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

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

  it('retries after delay', () => {
    const callback = jest.fn();
    scheduleRetry(callback);
    jest.advanceTimersByTime(5000);
    expect(callback).toHaveBeenCalled();
  });
});

8. Test Structure: describe/it Nesting and Naming

RIGHT — one top-level describe, nested per method

describe('OrderService', () => {
  let service: OrderService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new OrderService();
  });

  describe('createOrder', () => {
    it('creates an order with valid input', async () => {
      mockCreateOrder.mockResolvedValueOnce({ id: 1, status: 'received' });
      const result = await service.createOrder({ name: 'Test', items: [1] });
      expect(result.status).toBe('received');
      expect(mockCreateOrder).toHaveBeenCalledTimes(1);
    });

    it('throws on empty customer name', async () => {
      await expect(
        service.createOrder({ name: '', items: [1] })
      ).rejects.toThrow('name is required');
    });
  });

  describe('getOrder', () => {
    it('returns order by id', async () => {
      mockGetOrder.mockResolvedValueOnce({ id: 1, status: 'ready' });
      const order = await service.getOrder(1);
      expect(order.status).toBe('ready');
    });
  });
});

Structure rules

  • describe for grouping — one top-level per module/class, nested per method
  • beforeEach with jest.clearAllMocks() — always in beforeEach, never afterEach
  • it descriptions start with a verb — "creates", "throws", "returns" (not "should create")
  • Each it sets up its own mock return values with mockResolvedValueOnce

Configuration

// jest.config.ts
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
  setupFilesAfterSetup: ['./jest.setup.ts'],
};
// package.json scripts
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" } }

Supertest for API Tests

import request from 'supertest';
import { app } from '../src/app';

describe('Products API', () => {
  it('GET /api/products returns items', async () => {
    const res = await request(app).get('/api/products');
    expect(res.status).toBe(200);
    expect(res.body.data).toBeInstanceOf(Array);
  });

  it('POST /api/products validates input', async () => {
    const res = await request(app).post('/api/products').send({});
    expect(res.status).toBe(400);
    expect(res.body.error).toBeDefined();
  });
});

jest.spyOn() and Restoration

Use jest.spyOn() when you need to observe calls to an existing method without replacing the entire module. Always restore spies.

const spy = jest.spyOn(service, 'validate');
await service.process(data);
expect(spy).toHaveBeenCalledWith(data);
spy.mockRestore(); // Or use jest.restoreAllMocks() in beforeEach

When NOT to Mock

  • Don't mock the module under test — that tests nothing
  • Don't mock simple utility functions (string formatting, math) — just call them
  • Don't mock the database for integration tests — use a real test database

References

  • Jest Docs: Mock Functions
  • Jest Docs: Timer Mocks
  • Jest Docs: Testing Async Code
  • Jest Docs: expect.assertions

Checklist

  • jest.mock() used for external module dependencies (not manual injection)
  • Mocked functions cast with jest.MockedFunction<typeof fn>
  • jest.clearAllMocks() in beforeEach (not afterEach)
  • mockResolvedValueOnce / mockReturnValueOnce for per-test values
  • toHaveBeenCalledWith AND toHaveBeenCalledTimes for critical mocks
  • expect.assertions(n) for conditional/callback assertions
  • async/await for async tests (not .then())
  • await expect(fn()).rejects.toThrow() for rejected promises
  • jest.useFakeTimers() + jest.advanceTimersByTime() + jest.useRealTimers()
  • describe per module, nested per method
  • it() descriptions start with verbs
  • No test.only or describe.only committed

Verifiers

  • jest-structure — Structure Jest tests with describe/it blocks and proper isolation
Workspace
tessl-labs
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
tessl-labs/jest-testing badge