使用Jest和Supertest测试Next.js API:POST路由请求对象异常
Next.js + Jose JWT认证API路由Jest测试问题
问题描述
我在用Jest测试基于Next.js和Jose库实现的JWT认证POST API路由时遇到问题:接口在浏览器和Postman中运行正常,但Jest环境下无法获取正确的request对象,调用request.json()抛出TypeError,且无法识别响应对象以校验body、状态码和请求头,测试超时失败。改用http库创建服务器测试,结果依旧。
相关代码
route.js API路由文件
import { SignJWT } from "jose"; import { NextResponse } from "next/server"; import { getJwtSecretKey } from "@/libs/auth"; export async function POST(request) { try { const body = await request.json(); if (body.username === "test-name" && body.password === "password1234") { const token = await new SignJWT({ username: body.username, }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime("30s") .sign(getJwtSecretKey()); const response = NextResponse.json( { success: true }, { status: 200, headers: { "content-type": "application/json" } } ); response.cookies.set({ name: "token", value: token, path: "/", }); // Temporary stub for user profile response.cookies.set({ name: "user-name", value: body.username, path: "/api", }); return response; } return NextResponse.json( { success: false }, { status: 401, headers: { "content-type": "application/json" } } ); } catch (error) { return console.error(error) } }
初始测试文件
import request from 'supertest'; import { POST } from './route'; import { getJwtSecretKey } from '../../../libs/auth'; import * as Jose from "jose"; jest.mock('../../../libs/auth', () => ({ getJwtSecretKey: jest.fn(), })); jest.mock('next/dist/server/web/exports/next-response.js', () => ({ Request: jest.fn() })); jest.mock('jose', () => ({ SignJWT: jest.fn(() => Jose.SignJWT) })); describe('POST API Endpoint', () => { it('should return a success response with a token for valid credentials', async () => { jest.useFakeTimers('legacy'); getJwtSecretKey.mockReturnValue('your-secret-key'); const validCredentials = { username: 'test-name', password: 'password1234', }; const response = await request(POST) .post('/api/login') .set('Content-Type', 'application/json') .send(JSON.stringify(validCredentials)); ; expect(response.status).toBe(200); expect(response.body).toEqual({ success: true }); expect(response.headers['content-type']).toBe('application/json'); expect(response.header['set-cookie']).toContain('token='); }); // Other test cases here });
测试结果
console.error TypeError: request.json is not a function at Server.json (C:\Users\smiss-dev\projects\auth-jwt\src\app\api\login\route.js:7:36) at Server.emit (node:events:517:28) at parserOnIncoming (node:_http_server:1107:12) at HTTPParser.parserOnHeadersComplete (node:_http_common:119:17) 33 | return NextResponse.json({ success: false }); 34 | } catch (error) { > 35 | console.error(error) | ^ 36 | } 37 | } at Server.error (src/app/api/login/route.js:35:17) FAIL src/app/api/login/route.test.js (5.487 s) POST API Endpoint × should return a success response with a token for valid credentials (5002 ms) ● POST API Endpoint › should return a success response with a token for valid credentials thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." 15 | 16 | describe('POST API Endpoint', () => { > 17 | it('should return a success response with a token for valid credentials', async () => { | ^ 18 | jest.useFakeTimers('legacy'); 19 | getJwtSecretKey.mockReturnValue('your-secret-key'); 20 | at it (src/app/api/login/route.test.js:17:5) at Object.describe (src/app/api/login/route.test.js:16:1) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 5.911 s, estimated 6 s Ran all test suites matching /route/i. Jest did not exit one second after the test run has completed.
改用http库的测试代码
import request from 'supertest'; import { POST } from './route'; import { getJwtSecretKey } from '../../../libs/auth'; import { createServer } from "http"; import * as Jose from "jose"; jest.mock('../../../libs/auth', () => ({ getJwtSecretKey: jest.fn(), })); jest.mock('next/dist/server/web/exports/next-response.js', () => ({ Request: jest.fn() })); jest.mock('jose', () => ({ SignJWT: jest.fn(() => Jose.SignJWT) })); describe('POST API Endpoint', () => { let server; beforeAll(() => { server = createServer(POST); server.listen(); }); afterAll((done) => { server.close(done); }); it('should return a success response with a token for valid credentials', async () => { jest.useFakeTimers('legacy'); getJwtSecretKey.mockReturnValue('your-secret-key'); const validCredentials = { username: 'test-name', password: 'password1234', }; const response = await request(server) .post('/api/login') .set('Content-Type', 'application/json') .send(JSON.stringify(validCredentials)); ; expect(response.status).toBe(200); expect(response.body).toEqual({ success: true }); expect(response.headers['content-type']).toBe('application/json'); expect(response.header['set-cookie']).toContain('token='); }); // Other tests here });
jest.config.js
const nextJest = require('next/jest') const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment dir: './', }) const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testEnvironment: 'jest-environment-jsdom', } module.exports = createJestConfig(customJestConfig)
问题分析
- Request对象类型不匹配:Next.js App Router的API路由接收的是
NextRequest对象,而非Node.js原生HTTP请求对象。直接将POST函数传给supertest或http.createServer,会传入原生req/res,没有NextRequest的json()方法,导致报错。 - Mock配置错误:错误mock了
NextResponse,替换成仅含Request的对象,导致NextResponse.json无法正常工作;Jose的mock方式破坏了链式调用逻辑。 - Catch块无响应返回:route.js的catch块仅打印错误但不返回响应,导致请求挂起,触发测试超时。
- 测试环境不匹配:jest.config.js使用
jest-environment-jsdom(浏览器环境),但API路由属于服务器端代码,应使用jest-environment-node。
解决方案
1. 修复route.js的catch块
确保错误时返回合法响应,避免请求挂起:
// ... 其他代码 catch (error) { console.error(error); return NextResponse.json( { success: false, error: '服务器内部错误' }, { status: 500, headers: { "content-type": "application/json" } } ); }
2. 修改jest.config.js的测试环境
const nextJest = require('next/jest') const createJestConfig = nextJest({ dir: './', }) const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testEnvironment: 'jest-environment-node', // 改为node环境 } module.exports = createJestConfig(customJestConfig)
3. 编写正确的测试用例(直接测试POST函数,模拟NextRequest)
import { POST } from './route'; import { getJwtSecretKey } from '../../../libs/auth'; import { NextRequest } from 'next/server'; jest.mock('../../../libs/auth', () => ({ getJwtSecretKey: jest.fn(), })); // 正确mock Jose的SignJWT,保留链式调用 jest.mock('jose', () => { const original = jest.requireActual('jose'); return { ...original, SignJWT: jest.fn(() => ({ setProtectedHeader: jest.fn().mockReturnThis(), setIssuedAt: jest.fn().mockReturnThis(), setExpirationTime: jest.fn().mockReturnThis(), sign: jest.fn().mockResolvedValue('mocked-jwt-token'), })), }; }); describe('POST /api/login', () => { it('有效凭证返回200和token', async () => { getJwtSecretKey.mockReturnValue('test-secret-key'); // 模拟NextRequest对象 const mockRequest = new NextRequest('http://localhost/api/login', { method: 'POST', body: JSON.stringify({ username: 'test-name', password: 'password1234', }), headers: new Headers({ 'Content-Type': 'application/json' }), }); const response = await POST(mockRequest); // 校验响应状态和body expect(response.status).toBe(200); const responseBody = await response.json(); expect(responseBody).toEqual({ success: true }); expect(response.headers.get('content-type')).toBe('application/json'); // 校验Cookie const cookies = response.cookies.getAll(); expect(cookies.some(c => c.name === 'token' && c.value === 'mocked-jwt-token')).toBe(true); expect(cookies.some(c => c.name === 'user-name' && c.value === 'test-name')).toBe(true); }); it('无效凭证返回401', async () => { const mockRequest = new NextRequest('http://localhost/api/login', { method: 'POST', body: JSON.stringify({ username: 'wrong-user', password: 'wrong-pass', }), headers: new Headers({ 'Content-Type': 'application/json' }), }); const response = await POST(mockRequest); const responseBody = await response.json(); expect(response.status).toBe(401); expect(responseBody).toEqual({ success: false }); }); });
说明
这种方式直接测试API路由的POST函数,通过模拟NextRequest对象避免了与原生HTTP服务器的兼容问题,同时正确mock依赖库,确保测试逻辑独立可靠。
内容的提问来源于stack exchange,提问作者alex-bbk
相关产品推荐
相关产品推荐

