Files
claudekit/skills/testing/references/jest.md
T
2026-04-19 14:10:38 +07:00

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 vitest with @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.tssetupFilesAfterSetup vitest.config.tssetupFiles

Most tests migrate with a find-replace of jestvi and @jest/globalsvitest. Run npx vitest --reporter=verbose to catch edge cases.


Common Pitfalls

  1. Mock leaks between tests. Always call jest.restoreAllMocks() in afterEach. Without it, one test's mock infects the next.
  2. Forgetting await on async assertions. expect(fn()).rejects.toThrow() without await silently passes even if the promise resolves.
  3. Using jest.mock() with ESM. Module-level jest.mock() doesn't work reliably with ESM. Use jest.unstable_mockModule() or switch to Vitest.
  4. Testing implementation, not behavior. Asserting mock.toHaveBeenCalledTimes(3) tests internal calls, not outcomes. Assert on the return value or side effect instead.
  5. Slow transforms. Default ts-jest is slow. Switch to @swc/jest for 5-10x speedup with zero config change.
  6. Not closing NestJS app in E2E tests. Missing afterAll(() => app.close()) leaks connections and causes "open handle" warnings.
  7. Snapshot overuse. toMatchSnapshot() on large objects makes tests pass everything — any change auto-updates. Use targeted assertions instead.
  8. 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.

  • vitest — preferred runner for new TypeScript/React projects
  • nestjs — NestJS framework (Jest is the default runner)
  • react — React component patterns
  • testing-anti-patterns — test quality pitfalls (applies to Jest too)
  • test-driven-development — TDD methodology