12 KiB
Testing — Jest Patterns
Jest
Overview
Testing patterns for projects that use Jest as their test runner — primarily NestJS backends and legacy React projects. For new TypeScript/React projects, prefer vitest (faster, native ESM, Vite-aligned). This skill focuses on Jest-specific patterns, NestJS integration, and the Jest-to-Vitest migration path.
When to Use
- NestJS projects (Jest is the default test runner)
- Existing projects that already use Jest
- React component testing with Jest + Testing Library
- Debugging Jest configuration issues (ESM, TypeScript transforms, module resolution)
When NOT to Use
- New Vite/React/Next.js projects — use
vitest(better ESM support, faster) - Python testing — use
pytest - E2E browser testing — use
playwright - Cloudflare Workers — use
vitestwith@cloudflare/vitest-pool-workers
Quick Reference
| I need... | Go to |
|---|---|
| NestJS testing patterns | § NestJS Testing below |
| Mock patterns | § Mocking below |
| TypeScript config | § Configuration below |
| ESM troubleshooting | § ESM Gotchas below |
| Migration to Vitest | § Jest → Vitest Migration below |
Core Patterns
Test structure
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('should create a user with default role', () => {
const user = service.create({ email: 'test@example.com', name: 'Test' });
expect(user.role).toBe('member');
});
it('should throw on duplicate email', () => {
service.create({ email: 'test@example.com', name: 'A' });
expect(() => service.create({ email: 'test@example.com', name: 'B' }))
.toThrow('Email already exists');
});
});
Assertions
// Equality
expect(result).toBe(42); // strict ===
expect(result).toEqual({ id: '1' }); // deep equality
expect(result).toStrictEqual(obj); // deep + type equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(count).toBeGreaterThan(0);
expect(price).toBeCloseTo(9.99, 2);
// Strings
expect(message).toMatch(/error/i);
expect(message).toContain('failed');
// Arrays / objects
expect(arr).toContain('item');
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('email', 'test@example.com');
// Exceptions
expect(() => parse('{bad}')).toThrow(SyntaxError);
expect(() => validate({})).toThrow('Required');
// Async
await expect(fetchUser('missing')).rejects.toThrow('Not found');
await expect(fetchUser('exists')).resolves.toHaveProperty('id');
Mocking
jest.fn() — standalone mock function
const callback = jest.fn();
callback('arg1');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('arg1');
jest.spyOn() — spy on existing methods
const spy = jest.spyOn(service, 'findOne').mockResolvedValue(mockUser);
await controller.getUser('123');
expect(spy).toHaveBeenCalledWith('123');
spy.mockRestore(); // Restore original
jest.mock() — module mocking
// Auto-mock entire module
jest.mock('./email.service');
// Manual mock with implementation
jest.mock('./email.service', () => ({
EmailService: jest.fn().mockImplementation(() => ({
send: jest.fn().mockResolvedValue({ messageId: 'msg_123' }),
})),
}));
Mock return values
const mock = jest.fn();
mock.mockReturnValue(42); // Sync
mock.mockReturnValueOnce(1); // First call only
mock.mockResolvedValue({ ok: true }); // Async
mock.mockRejectedValue(new Error()); // Async throw
mock.mockImplementation((x) => x * 2); // Custom logic
Clear vs Reset vs Restore
| Method | Clears calls | Resets implementation | Restores original |
|---|---|---|---|
mockClear() |
yes | no | no |
mockReset() |
yes | yes (returns undefined) | no |
mockRestore() |
yes | yes | yes (spyOn only) |
Use jest.restoreAllMocks() in afterEach to avoid mock leaks.
NestJS Testing
Unit test a service
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';
import { NotFoundException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
let prisma: jest.Mocked<PrismaService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: PrismaService,
useValue: {
user: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
},
],
}).compile();
service = module.get(UsersService);
prisma = module.get(PrismaService);
});
it('throws NotFoundException for missing user', async () => {
prisma.user.findUnique.mockResolvedValue(null);
await expect(service.findOne('missing')).rejects.toThrow(NotFoundException);
});
it('returns user when found', async () => {
const mockUser = { id: '1', email: 'test@example.com', name: 'Test' };
prisma.user.findUnique.mockResolvedValue(mockUser);
const result = await service.findOne('1');
expect(result).toEqual(mockUser);
expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
E2E test a controller
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Users (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
await app.init();
});
afterAll(() => app.close());
it('POST /users creates user', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201)
.expect((res) => expect(res.body).toHaveProperty('id')));
it('POST /users rejects invalid payload', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'bad' })
.expect(400));
});
Test a guard
import { ExecutionContext } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtService } from '@nestjs/jwt';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let jwtService: jest.Mocked<JwtService>;
beforeEach(() => {
jwtService = { verifyAsync: jest.fn() } as any;
guard = new JwtAuthGuard(jwtService);
});
const mockContext = (authHeader?: string): ExecutionContext => ({
switchToHttp: () => ({
getRequest: () => ({
headers: { authorization: authHeader },
}),
}),
}) as any;
it('rejects missing token', async () => {
await expect(guard.canActivate(mockContext())).rejects.toThrow('Missing bearer token');
});
it('accepts valid token', async () => {
jwtService.verifyAsync.mockResolvedValue({ sub: 'user_1', role: 'admin' });
await expect(guard.canActivate(mockContext('Bearer valid.jwt.token'))).resolves.toBe(true);
});
});
Configuration
TypeScript with ts-jest
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts', '**/*.test.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.module.ts',
'!src/main.ts',
'!src/**/*.dto.ts',
'!src/**/*.entity.ts',
],
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
};
export default config;
SWC transform (faster)
Replace ts-jest with @swc/jest for 5-10x faster transforms:
// jest.config.ts
const config: Config = {
transform: {
'^.+\\.tsx?$': ['@swc/jest'],
},
// ... rest same
};
React + Testing Library
// jest.config.ts
const config: Config = {
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
transform: { '^.+\\.tsx?$': ['@swc/jest'] },
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
'\\.(jpg|png|svg)$': '<rootDir>/__mocks__/fileMock.ts',
},
};
// jest.setup.ts
import '@testing-library/jest-dom';
ESM Gotchas
Jest's ESM support is still experimental. Common issues and fixes:
| Problem | Fix |
|---|---|
SyntaxError: Cannot use import |
Add transform with ts-jest or @swc/jest |
Module not found for .js imports |
Set moduleNameMapper or use ts-jest with useESM: true |
jest.mock() doesn't work with ESM |
Use jest.unstable_mockModule() (experimental) |
Dynamic import() in tests |
Set transform to handle the syntax |
__dirname undefined |
ESM doesn't have __dirname; use import.meta.url + fileURLToPath |
If fighting ESM issues takes more than 30 minutes, migrate to Vitest. Vitest handles ESM natively and is a near-drop-in replacement.
Jest → Vitest Migration
For projects outgrowing Jest's ESM limitations or wanting faster transforms:
| Jest | Vitest |
|---|---|
jest.fn() |
vi.fn() |
jest.mock('./mod') |
vi.mock('./mod') |
jest.spyOn(obj, 'method') |
vi.spyOn(obj, 'method') |
jest.useFakeTimers() |
vi.useFakeTimers() |
jest.config.ts |
vitest.config.ts |
@jest/globals |
vitest |
ts-jest / @swc/jest |
Not needed (native TS) |
jest.setup.ts → setupFilesAfterSetup |
vitest.config.ts → setupFiles |
Most tests migrate with a find-replace of jest → vi and @jest/globals → vitest. Run npx vitest --reporter=verbose to catch edge cases.
Common Pitfalls
- Mock leaks between tests. Always call
jest.restoreAllMocks()inafterEach. Without it, one test's mock infects the next. - Forgetting
awaiton async assertions.expect(fn()).rejects.toThrow()withoutawaitsilently passes even if the promise resolves. - Using
jest.mock()with ESM. Module-leveljest.mock()doesn't work reliably with ESM. Usejest.unstable_mockModule()or switch to Vitest. - Testing implementation, not behavior. Asserting
mock.toHaveBeenCalledTimes(3)tests internal calls, not outcomes. Assert on the return value or side effect instead. - Slow transforms. Default
ts-jestis slow. Switch to@swc/jestfor 5-10x speedup with zero config change. - Not closing NestJS app in E2E tests. Missing
afterAll(() => app.close())leaks connections and causes "open handle" warnings. - Snapshot overuse.
toMatchSnapshot()on large objects makes tests pass everything — any change auto-updates. Use targeted assertions instead. - Running Jest where Vitest fits. For new Vite/React/Next.js projects, Vitest is strictly better (native ESM, faster, same API). Only use Jest when the framework mandates it (NestJS) or the project already depends on it.
Related Skills
vitest— preferred runner for new TypeScript/React projectsnestjs— NestJS framework (Jest is the default runner)react— React component patternstesting-anti-patterns— test quality pitfalls (applies to Jest too)test-driven-development— TDD methodology