Jest testing patterns — test structure, mocking, async testing, snapshot
99
99%
Does it follow best practices?
Impact
99%
1.26xAverage score across 6 eval scenarios
Passed
No known issues
Test structure, mocking, async patterns, and configuration for Jest.
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.
// Manually creating mocks and injecting via constructor — bypasses module system
const mockRepo = { findUser: jest.fn() };
const service = new UserService(mockRepo); // No jest.mock() calljest.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.
afterEach(() => {
jest.clearAllMocks(); // Runs after — if a test fails, cleanup may be skipped
});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.
Use mockResolvedValueOnce / mockReturnValueOnce for per-test setup. Reserve mockResolvedValue only for default fallback values.
mockGetUser.mockResolvedValue({ id: 1, name: 'Alice' }); // Persists across tests!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.
Always verify both the arguments and the call count of critical mocks.
expect(mockSave).toHaveBeenCalledWith({ id: 1, name: 'Alice' });
// Missing call count check — mock may have been called multiple timesexpect(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).
When assertions run inside callbacks, .catch() blocks, or conditional branches, use expect.assertions(n) to guarantee they actually execute.
it('handles error', async () => {
try {
await service.riskyOperation();
} catch (e) {
expect(e.message).toBe('failed'); // Never runs if no error thrown!
}
});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.
it('fetches data', () => {
return service.fetchData().then((data) => {
expect(data).toBeDefined(); // Works but harder to read and debug
});
});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!
});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();
});it('retries after delay', () => {
jest.useFakeTimers();
scheduleRetry(callback);
jest.advanceTimersByTime(5000);
expect(callback).toHaveBeenCalled();
// Missing jest.useRealTimers() — breaks subsequent tests!
});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();
});
});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');
});
});
});describe for grouping — one top-level per module/class, nested per methodbeforeEach with jest.clearAllMocks() — always in beforeEach, never afterEachit descriptions start with a verb — "creates", "throws", "returns" (not "should create")it sets up its own mock return values with mockResolvedValueOnce// 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" } }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();
});
});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 beforeEachjest.mock() used for external module dependencies (not manual injection)jest.MockedFunction<typeof fn>jest.clearAllMocks() in beforeEach (not afterEach)mockResolvedValueOnce / mockReturnValueOnce for per-test valuestoHaveBeenCalledWith AND toHaveBeenCalledTimes for critical mocksexpect.assertions(n) for conditional/callback assertionsasync/await for async tests (not .then())await expect(fn()).rejects.toThrow() for rejected promisesjest.useFakeTimers() + jest.advanceTimersByTime() + jest.useRealTimers()describe per module, nested per methodit() descriptions start with verbstest.only or describe.only committed