| name | integration-test |
| description | Write integration tests with Jest for API routes and database operations. Use when testing backend logic, API endpoints, or data layer. |
Integration Test (Jest)
Standard Structure
src/__tests__/integration/
├── books.test.ts # All book-related API tests
├── categories.test.ts # All category API tests
├── auth.test.ts # Authentication API tests
└── cart.test.ts # Cart/checkout API tests
Naming Convention
- File:
{feature}.test.ts (e.g., featured-books.test.ts, categories.test.ts)
- Describe:
{Feature} API (e.g., Books API, Categories API)
- Nested describe:
{HTTP_METHOD} /api/{route} (e.g., GET /api/books/featured)
One File Per Feature
Consolidate ALL related API tests into ONE file:
describe('Books API', () => {
describe('GET /api/books/featured', () => {...});
describe('GET /api/books/bestsellers', () => {...});
});
⭐ Branch Coverage Strategy (3-4 tests per API)
Focus on BRANCH COVERAGE, not quantity! Each API needs only 3 tests:
| Branch | Test Case | Priority |
|---|
| Happy path | Valid request → success data | ⭐⭐⭐ MUST |
| Empty case | No data → empty array | ⭐⭐⭐ MUST |
| Error case | DB error → error response | ⭐⭐ SHOULD |
Example: 3 Tests = 100% Branch Coverage
describe('GET /api/books/featured', () => {
beforeEach(() => jest.clearAllMocks());
it('returns books when data exists', async () => {
(prisma.book.findMany as jest.Mock).mockResolvedValue([
{ id: '1', title: 'Book 1' }
]);
const response = await GET();
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toHaveLength(1);
});
it('returns empty array when no books', async () => {
(prisma.book.findMany as jest.Mock).mockResolvedValue([]);
const response = await GET();
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toEqual([]);
});
it('handles database error', async () => {
(prisma.book.findMany as jest.Mock).mockRejectedValue(new Error('DB Error'));
const response = await GET();
const data = await response.json();
expect(data.success).toBe(false);
});
});
Anti-pattern: Redundant Tests
it('returns featured books', async () => {...});
it('returns books with correct fields', async () => {...});
it('returns books sorted by date', async () => {...});
it('returns max 10 books', async () => {...});
⭐ FLEXIBLE ASSERTIONS (Avoid Hardcoded Values)
NEVER hardcode calculated values - they cause mock-assertion mismatch!
const mockReviews = [{ rating: 5 }, { rating: 4 }];
expect(data.averageRating).toBe(5);
expect(data.data[0].price).toBe(29.99);
expect(data.data[0].totalSold).toBe(100);
expect(data.data[0].averageRating).toBe(4.5);
Use Flexible Matchers Instead:
expect(data.data[0]).toMatchObject({
id: expect.any(String),
title: expect.any(String),
price: expect.any(Number),
averageRating: expect.any(Number),
});
expect(data.data).toHaveLength(1);
expect(data.data[0].averageRating).toBeGreaterThanOrEqual(0);
expect(data.data[0].averageRating).toBeLessThanOrEqual(5);
expect(data.data[0].totalSold).toBeDefined();
expect(data.data[0].category).toBeDefined();
expect(data.success).toBe(true);
Quick Reference:
| Instead of | Use |
|---|
.toBe(4.5) | .toBeGreaterThanOrEqual(0) |
.toBe(100) | .toBeDefined() or expect.any(Number) |
.toBe('exact string') | .toContain('substring') or expect.any(String) |
.toEqual({...exact...}) | .toMatchObject({...partial...}) |
⛔ CRITICAL: NEVER CHECK response.status
NextResponse.status is ALWAYS undefined in Jest environment. This is a known limitation.
const response = await GET(request);
expect(response.status).toBe(200);
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toBeDefined();
expect(data.error).toBeUndefined();
const response = await POST(invalidRequest);
const data = await response.json();
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
WHY: When calling Next.js route handlers directly (not via HTTP), the Response object's .status property is not populated correctly in Jest's mock environment.
⚠️ CRITICAL RULES - READ FIRST
- DO NOT create config files (jest.config.*, tsconfig.json)
- Config files ALREADY EXIST in project - use them as-is
- ONLY create TEST files: *.test.ts
- READ SOURCE CODE FIRST - Check actual exports, types, function signatures before writing tests
- DO NOT INVENT APIs - Only test routes/functions that actually exist in the codebase
- NEVER use
response.status - it's always undefined (see above)
⚠️ Response Methods in Jest
In Jest mock environment, use response.json() directly - do NOT use response.text().
const response = await GET(request);
const text = await response.text();
const response = await GET(request);
expect(response.status).toBe(200);
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toEqual(expected);
BEST PRACTICE: For integration tests, mock at the database layer (Prisma) and check data.success or data.error instead of response.status.
⚠️ API Response Wrapper - UNDERSTAND THE FLOW
This project uses successResponse() helper that wraps data. Understand this flow:
mockFindMany.mockResolvedValue([
{ id: '1', title: 'Book 1', author: 'Author 1' },
{ id: '2', title: 'Book 2', author: 'Author 2' },
]);
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toHaveLength(2);
expect(data.data[0].title).toBe('Book 1');
expect(data).toHaveLength(2);
expect(data[0].title).toBe('Book 1');
expect(data.length).toBe(2);
REMEMBER: data is always { success: boolean, data: T }, never raw array!
⚠️ GET Routes with Query Parameters (searchParams)
Many Next.js routes use request.nextUrl.searchParams. You MUST use NextRequest:
import { NextRequest } from 'next/server';
import { GET } from '@/app/api/books/search/route';
it('searches books by query', async () => {
const request = new NextRequest('http://localhost/api/books/search?q=math&page=1');
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toBeDefined();
});
const request = new Request('http://localhost/api/search');
const request = new NextRequest('http://localhost/api/search');
RULE: If route uses request.nextUrl.searchParams, you MUST use NextRequest with query params in URL.
⚠️ TYPESCRIPT STRICT RULES
const mockFn = jest.fn((...args: unknown[]) => mockImpl(...args));
const handler = (req: Request, params: { id: string }) => {...};
const mockFn = jest.fn((...args) => mockImpl(...args));
const handler = (req, params) => {...};
⚠️ IMPORT RULES
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
import { GET, POST } from '@/app/api/users/route';
import getServerSession from 'next-auth';
import prisma from '@/lib/prisma';
When to Use
- Testing API route handlers (GET, POST, PUT, DELETE)
- Testing database operations (Prisma)
- Testing service layer logic
- Testing with mocked external services
File Location (FIXED - DO NOT CHANGE)
src/__tests__/integration/ # Integration tests (Jest)
IMPORTANT: Always use src/__tests__/integration/ for integration tests. Do NOT use other folders like __tests__/integration/ or tests/.
Test Structure
import { POST } from '@/app/api/users/route';
const mockCreate = jest.fn();
const mockFindMany = jest.fn();
const mockFindUnique = jest.fn();
jest.mock('@/lib/prisma', () => ({
prisma: {
user: {
create: (...args: unknown[]) => mockCreate(...args),
findMany: (...args: unknown[]) => mockFindMany(...args),
findUnique: (...args: unknown[]) => mockFindUnique(...args),
},
},
}));
describe('Story: {story_title}', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('POST /api/users', () => {
it('creates user with valid data', async () => {
const mockUser = { id: 'test-1', name: 'Test User', email: 'test@example.com' };
mockCreate.mockResolvedValue(mockUser);
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test User', email: 'test@example.com' }),
});
const response = await POST(request);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.data).toEqual(mockUser);
expect(mockCreate).toHaveBeenCalledWith({
data: { name: 'Test User', email: 'test@example.com' },
});
});
it('returns error for invalid data', async () => {
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({}),
});
const response = await POST(request);
const data = await response.json();
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
});
});
});
Mock Patterns
Prisma Mock Setup
const mockFindUnique = jest.fn();
const mockCreate = jest.fn();
const mockUpdate = jest.fn();
const mockDelete = jest.fn();
const mockFindMany = jest.fn();
jest.mock('@/lib/prisma', () => ({
prisma: {
user: {
findUnique: (...args: unknown[]) => mockFindUnique(...args),
create: (...args: unknown[]) => mockCreate(...args),
update: (...args: unknown[]) => mockUpdate(...args),
delete: (...args: unknown[]) => mockDelete(...args),
findMany: (...args: unknown[]) => mockFindMany(...args),
},
},
}));
Fetch Mock
global.fetch = jest.fn();
(fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => ({ data: 'test' }),
});
NextAuth Session Mock
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}));
(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'user-1', email: 'test@example.com' },
});
AAA Pattern (Arrange-Act-Assert)
- Arrange: Set up test data, mocks, preconditions
- Act: Execute the code being tested
- Assert: Verify expected outcomes
Commands
pnpm test
pnpm test tests/integration
pnpm test --coverage
pnpm test --watch
Important Rules
- NO ESM packages - Don't use uuid, nanoid. Use hardcoded IDs:
"test-id-123"
- Clear mocks - Always
jest.clearAllMocks() in beforeEach
- Async/await - Always await async operations
- Isolation - Each test should be independent
Anti-Patterns - DO NOT DO
Don't create helper functions for response extraction
async function extractResponse(response: Response) {
const data = await response.json();
return { status: response.status, data };
}
const { status, data } = await extractResponse(response);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
Don't check exact Prisma query structure
expect(mockFindMany).toHaveBeenCalledWith({
where: { OR: [{ title: { contains: 'test', mode: 'insensitive' } }] },
skip: 0,
take: 20,
include: { category: { select: { id: true, name: true } } },
orderBy: [{ featured: 'desc' }, { createdAt: 'desc' }],
});
expect(mockFindMany).toHaveBeenCalled();
expect(data).toHaveLength(2);
expect(data[0].title).toBe('Expected Title');
Don't use Date objects in mock data
const mockBook = {
id: 'book-1',
createdAt: new Date('2023-01-15'),
updatedAt: new Date('2023-01-15'),
};
const mockBook = {
id: 'book-1',
createdAt: '2023-01-15T00:00:00.000Z',
updatedAt: '2023-01-15T00:00:00.000Z',
};
Keep mock data minimal
const mockUser = {
id: 'user-1',
name: 'Test User',
email: 'test@example.com',
phone: '123-456-7890',
address: '123 Main St',
city: 'Test City',
country: 'Test Country',
createdAt: '2023-01-01',
updatedAt: '2023-01-01',
role: 'user',
isActive: true,
};
const mockUser = { id: 'user-1', name: 'Test User' };
References
mock-patterns.md - Advanced mocking patterns for Prisma, Auth, External APIs