一键导入
jest-backend-testing
Write Jest backend tests for Next.js API routes following established patterns. Use for testing API handlers, lib utilities, and backend logic.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Write Jest backend tests for Next.js API routes following established patterns. Use for testing API handlers, lib utilities, and backend logic.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Implementation conventions for backend API routes and lib utilities. Loaded by tdd-implementer when the layer is backend.
Write Cypress end-to-end tests for business processes and cross-cutting infrastructure. Use for testing complete business workflows, authentication flows, and UI state verification.
Implementation conventions for frontend React components, hooks, and pages. Loaded by tdd-implementer when the layer is frontend.
Write Jest frontend tests for React components and hooks using React Testing Library. Use for testing pages, components, and custom hooks.
Outside-In TDD starting from E2E tests, drilling down to unit tests. Auto-triggers for new features. Trigger phrases include "implement", "add feature", "build", "create functionality". Does NOT trigger for bug fixes, documentation, or configuration changes.
Instructions for adjusting outside-in-tdd skills and agents
| name | jest-backend-testing |
| description | Write Jest backend tests for Next.js API routes following established patterns. Use for testing API handlers, lib utilities, and backend logic. |
Write backend tests for Next.js API routes and library utilities using the established patterns in this codebase.
See .claude/agents/tdd-test-writer-contract.md — "frontend / backend" section for field definitions.
Write ALL tests needed for this layer in one go. This reduces RED→GREEN→REFACTOR cycles:
console.log (and console.warn/console.error if used) in beforeEach when the unit under test logs to the console: jest.spyOn(console, 'log').mockImplementation(). This prevents test output pollution. Jest's restoreAllMocks handles cleanup.Tests mirror the source directory structure:
src/pages/api/todos/index.ts → test/pages/api/todos/index.test.ts
src/lib/todo-utils.ts → test/lib/todo-utils.test.ts
# Run a specific test file
npm run test:unit:spec path/to/test.test.ts
# Run all backend tests
npm run test:unit -- --selectProjects backend
# Run tests matching a pattern
npm run test:unit:spec -- -t "Todos API"
These failures mean the test is correct and implementation is missing:
| Failure Type | Example | Why It's Right |
|---|---|---|
| Wrong status | Expected: 200, Received: 404 | API route not implemented |
| Wrong response | Expected: X, Received: Y | Logic not implemented |
| Missing function | TypeError: X is not a function | Function needs to be created |
| Missing Prisma model | The table 'X' does not exist / PrismaClientValidationError | New model needs schema + migration (test uses as any to bypass compile-time types) |
These failures mean the test itself is broken:
| Failure Type | Example | Action |
|---|---|---|
| Syntax error | SyntaxError: Unexpected token | Fix the test code |
| Import error | Cannot find module '@/lib/...' | Fix imports |
| Type error | Type 'X' is not assignable to type 'Y' | Fix types in test (exception: for new Prisma models, use as any to bypass — see Right Failures) |
Key Question: "Is this failing because implementation is missing, or because my test is broken?"
import {NextApiRequest, NextApiResponse} from 'next';
import handler from '@/pages/api/your-api/index';
import {createMocks, MockRequest, MockResponse, Body, Cookies, RequestMethod} from 'node-mocks-http';
import prisma from "@/lib/cached-prisma-client";
import {User} from '@prisma/client';
describe('Your API', () => {
let testUser: User;
beforeEach(async () => {
const uniqueId = Date.now() + Math.random();
testUser = await prisma.user.create({
data: {name: `Test User ${uniqueId}`, email: `test-${uniqueId}@example.com`}
});
});
function createApiRequest(options: {
method?: RequestMethod;
body?: object;
query?: object;
cookies?: Cookies;
}): {
req: MockRequest<NextApiRequest>,
res: MockResponse<NextApiResponse>
} {
return createMocks<NextApiRequest, NextApiResponse>({
method: options.method || 'GET',
body: options.body ? JSON.stringify(options.body) as unknown as Body : undefined,
query: options.query,
cookies: options.cookies,
});
}
describe('GET /api/your-api', () => {
it('returns expected data', async () => {
const {req, res} = createApiRequest({});
await handler(req, res);
expect(res.statusCode).toBe(200);
const data = JSON.parse(res._getData());
expect(data).toEqual(expect.objectContaining({
// expected shape
}));
});
});
});
import {yourFunction} from "@/lib/your-utils";
describe('your-utils', () => {
function buildTestInput(options: Partial<InputType>): InputType {
return {
defaultField: 'default',
...options,
};
}
describe('yourFunction', () => {
it('handles normal case', () => {
const input = buildTestInput({field: 'value'});
const result = yourFunction(input);
expect(result).toEqual(expectedValue);
});
});
});
All API handlers are wrapped with withErrorHandling from @/lib/api-error-handler. Use the shared describeErrorHandling helper:
import {describeErrorHandling} from '@test/helpers/error-handling-assertions';
describeErrorHandling(handler, {
createRequest: () => createGetRequest(),
causeError: () => {
jest.spyOn(prisma.todo, 'findMany').mockRejectedValueOnce(new Error('DB error'));
},
});
The test environment uses jest-prisma which wraps each test in a transaction that automatically rolls back. This means:
prisma via @/lib/cached-prisma-client - it's already set up to use the transactional clientprisma.<model> directly. The mock uses a getter that resolves to the current test's transactional client; storing prisma.<model> in a variable captures a stale reference.await import(...)) — use static imports with jest.mock(). Dynamic imports bypass jest mocks and can resolve to unmocked modules.jestPrisma.originalClient — it bypasses transaction isolation and permanently mutates the dev database.beforeEach — the dev DB may contain data from manual testing. Tests that assert on record counts must delete relevant records in beforeEach using the transactional prisma client (deletes are rolled back after each test). Delete child records first (FK constraints).import prisma from "@/lib/cached-prisma-client";
// BAD — captures delegate before transaction is active:
// const todoModel = prisma.todo;
// GOOD — resolves lazily each time:
const todo = await prisma.todo.create({
data: {title: 'Test Todo', userId: testUser.id}
});
expect(res.statusCode).toBe(200);
const data = JSON.parse(res._getData());
expect(data).toEqual({message: 'Success'});
expect(data).toEqual(expect.objectContaining({id: expect.any(Number)}));
expect(data.items).toHaveLength(2);
import {NextApiRequest, NextApiResponse} from 'next';
import {createMocks, MockRequest, MockResponse, Body, Cookies, RequestMethod} from 'node-mocks-http';
import prisma from "@/lib/cached-prisma-client";
import {User, Todo} from '@prisma/client';
import {v4 as uuid} from 'uuid';
When writing backend tests that reference a Prisma model that doesn't exist yet, use as any to bypass TypeScript compilation so the test fails at runtime (the right failure) instead of at compile time (the wrong failure):
// Model doesn't exist yet — use `as any` so the test fails at runtime
const entry = await (jestPrisma.client as any).category.create({
data: { name: 'Work' }
})
The runtime error ("The table Category does not exist" or similar) IS the right failure — it tells the implementer exactly what Prisma model to create.
Lint exception: eslint-disable-next-line is permitted on lines using as any for new Prisma models — the refactorer removes these after the implementer creates the model.
SWC hoists jest.mock() calls above const declarations. Never declare a mock variable above jest.mock and reference it inside the factory — it will be in the temporal dead zone at runtime. Instead, inline jest.fn() inside the factory and import the mocked function directly:
// Correct — inline jest.fn, import the mock
import {someFunction} from '@/lib/some-module';
jest.mock('@/lib/some-module', () => ({
someFunction: jest.fn(),
}));
expect(someFunction).toHaveBeenCalled();
(someFunction as jest.Mock).mockReturnValue('value');
// Wrong — variable is in temporal dead zone when factory runs
const mockSomeFunction = jest.fn();
jest.mock('@/lib/some-module', () => ({
someFunction: mockSomeFunction, // ReferenceError!
}));
as any is ONLY permitted for new Prisma models (above). In all other test code, use as unknown as SpecificType when a type cast is needed. Check existing test files in the codebase for the correct target type. The project enforces @typescript-eslint/no-explicit-any.