
integration-test
by xmark168
SKILL.md
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:
// CORRECT: One file for all book APIs
// src/__tests__/integration/books.test.ts
describe('Books API', () => {
describe('GET /api/books/featured', () => {...});
describe('GET /api/books/bestsellers', () => {...});
});
// WRONG: Multiple files for same feature
// featured-books.test.ts
// bestsellers.test.ts
⭐ 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());
// Branch 1: Happy path
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);
});
// Branch 2: Empty case
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([]);
});
// Branch 3: Error case
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);
});
});
// Total: 3 tests covering ALL branches!
Anti-pattern: Redundant Tests
// WRONG: Multiple tests for same branch (happy path)
it('returns featured books', async () => {...}); // Happy path
it('returns books with correct fields', async () => {...}); // Same branch!
it('returns books sorted by date', async () => {...}); // Same branch!
it('returns max 10 books', async () => {...}); // Same branch!
// 4 tests but only 1 branch covered!
⭐ FLEXIBLE ASSERTIONS (Avoid Hardcoded Values)
NEVER hardcode calculated values - they cause mock-assertion mismatch!
// WRONG - Hardcoded value causes failures
const mockReviews = [{ rating: 5 }, { rating: 4 }]; // avg = 4.5
expect(data.averageRating).toBe(5); // FAILS! Mock gives 4.5, not 5
// WRONG - Hardcoded specific values
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:
// Check structure exists (RECOMMENDED)
expect(data.data[0]).toMatchObject({
id: expect.any(String),
title: expect.any(String),
price: expect.any(Number),
averageRating: expect.any(Number),
});
// Check array has items
expect(data.data).toHaveLength(1); // or toBeGreaterThan(0)
// Check value ranges (not exact)
expect(data.data[0].averageRating).toBeGreaterThanOrEqual(0);
expect(data.data[0].averageRating).toBeLessThanOrEqual(5);
// Check field exists
expect(data.data[0].totalSold).toBeDefined();
expect(data.data[0].category).toBeDefined();
// Check boolean/success flags (OK to be exact)
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.
// ⛔ WRONG - WILL ALWAYS FAIL (response.status is undefined)
const response = await GET(request);
expect(response.status).toBe(200); // FAILS: Expected 200, Received: undefined
// CORRECT - Check data.success or data properties instead
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true); // Check API response structure
expect(data.data).toBeDefined(); // Check data exists
expect(data.error).toBeUndefined(); // No error for success
// For error cases - Check error response structure
const response = await POST(invalidRequest);
const data = await response.json();
expect(data.success).toBe(false); // API returns success: false
expect(data.error).toBeDefined(); // Error message exists
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().
// WRONG - response.text() is not available in Jest mock
const response = await GET(request);
const text = await response.text(); // ERROR: response.text is not a function
// WRONG - response.status is undefined
const response = await GET(request);
expect(response.status).toBe(200); // FAILS: undefined !== 200
// CORRECT - Use response.json() and check data properties
const response = await GET(request);
const data = await response.json();
expect(data.success).toBe(true); // Check success flag
expect(data.data).toEqual(expected); // Check actual data
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:
// 1. Prisma mock returns RAW data (what database would return)
mockFindMany.mockResolvedValue([
{ id: '1', title: 'Book 1', author: 'Author 1' },
{ id: '2', title: 'Book 2', author: 'Author 2' },
]);
// 2. Route handler calls Prisma, gets raw data, wraps using successResponse()
// Inside route.ts: return successResponse(books);
// This returns: { success: true, data: [{id: '1'...}, {id: '2'...}] }
// 3. Test asserts on WRAPPED response structure
const response = await GET(request);
const data = await response.json();
// CORRECT - Check wrapped response
expect(data.success).toBe(true); // Check success flag
expect(data.data).toHaveLength(2); // Check data array length
expect(data.data[0].title).toBe('Book 1'); // Check data content
// ⛔ WRONG - Checking as if raw array (COMMON MISTAKE!)
expect(data).toHaveLength(2); // FAILS! data is object, not array
expect(data[0].title).toBe('Book 1'); // FAILS! data[0] is undefined
expect(data.length).toBe(2); // FAILS! data.length is undefined
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 () => {
// CORRECT - Create NextRequest with URL containing query params
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();
});
// WRONG - Basic Request doesn't have nextUrl property
const request = new Request('http://localhost/api/search');
// In route.ts: request.nextUrl.searchParams.get('q')
// ERROR: Cannot read property 'searchParams' of undefined!
// WRONG - Missing query params in URL
const request = new NextRequest('http://localhost/api/search');
// route expects ?q=... but URL has no params → validation error or empty results
RULE: If route uses request.nextUrl.searchParams, you MUST use NextRequest with query params in URL.
⚠️ TYPESCRIPT STRICT RULES
// CORRECT - Explicit types for ALL parameters
const mockFn = jest.fn((...args: unknown[]) => mockImpl(...args));
const handler = (req: Request, params: { id: string }) => {...};
// WRONG - Implicit any (will cause TS errors)
const mockFn = jest.fn((...args) => mockImpl(...args)); // Error: implicit any
const handler = (req, params) => {...}; // Error: implicit any
⚠️ IMPORT RULES
// CORRECT imports
import { getServerSession } from 'next-auth'; // Named import
import { prisma } from '@/lib/prisma'; // Named import
import { GET, POST } from '@/app/api/users/route'; // Named imports for route handlers
// WRONG imports - DO NOT USE
import getServerSession from 'next-auth'; // Wrong: not default export
import prisma from '@/lib/prisma'; // Wrong: check actual export
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';
// Mock functions at top level (outside describe)
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 () => {
// Arrange
const mockUser = { id: 'test-1', name: 'Test User', email: 'test@example.com' };
mockCreate.mockResolvedValue(mockUser);
// Act
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();
// Assert - Check data properties, NOT response.status
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 () => {
// Arrange - empty body
const request = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({}),
});
// Act
const response = await POST(request);
const data = await response.json();
// Assert - Check error response, NOT response.status
expect(data.success).toBe(false);
expect(data.error).toBeDefined();
});
});
});
Mock Patterns
Prisma Mock Setup
// At top of file, outside describe
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();
// In test
(fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => ({ data: 'test' }),
});
NextAuth Session Mock
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}));
// In test
(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 # All tests
pnpm test tests/integration # Integration only
pnpm test --coverage # With coverage
pnpm test --watch # Watch mode
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
// WRONG - Unnecessary abstraction, prone to errors
async function extractResponse(response: Response) {
const data = await response.json();
return { status: response.status, data };
}
const { status, data } = await extractResponse(response);
// CORRECT - Use directly, simple and clear
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
Don't check exact Prisma query structure
// WRONG - Brittle, breaks on any refactor
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' }],
});
// CORRECT - Check behavior via response data
expect(mockFindMany).toHaveBeenCalled();
expect(data).toHaveLength(2);
expect(data[0].title).toBe('Expected Title');
Don't use Date objects in mock data
// WRONG - Date comparison issues in assertions
const mockBook = {
id: 'book-1',
createdAt: new Date('2023-01-15'),
updatedAt: new Date('2023-01-15'),
};
// CORRECT - Use ISO strings for dates
const mockBook = {
id: 'book-1',
createdAt: '2023-01-15T00:00:00.000Z',
updatedAt: '2023-01-15T00:00:00.000Z',
};
Keep mock data minimal
// WRONG - Too many fields, hard to maintain
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,
// ... more fields
};
// CORRECT - Only include fields being tested
const mockUser = { id: 'user-1', name: 'Test User' };
References
mock-patterns.md- Advanced mocking patterns for Prisma, Auth, External APIs
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon