Jest单元测试express-validator时验证结果为空,该如何正确测试?
如何用Jest测试express-validator的参数校验逻辑
问题描述
我正在用Jest为Express API的控制器路由编写单元测试,控制器里调用了validationResult(req),但运行测试时validationResult返回空值,触发不了预期的参数校验逻辑。
控制器代码
import { Request, Response, NextFunction } from "express"; import { validationResult } from "express-validator"; import TDS from "../DomainLogic/TasmanianDevilService"; async function createTasmanianDevil ( req: Request, res: Response, ) { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json(errors.array()); } console.log(errors); const result = await TDS.createTasmanianDevilAsync(req.body.attribute); if(result === undefined) { return res.status(500).json('Error'); } return res.status(200).json( result ); }; export default { createTasmanianDevil };
验证器代码
import express from "express"; import controller from "../controllers/TasmanianDevilController"; import { check, body, query } from "express-validator" const router = express.Router(); router.post('/createTasmanianDevil', [ body('attribute').isInt(), ], controller.createTasmanianDevil); export = router;
Jest测试代码
import TDC from "../../controllers/TasmanianDevilController"; import TDS from "../../DomainLogic/TasmanianDevilService"; import { getMockReq, getMockRes } from "@jest-mock/express"; jest.mock("../../DomainLogic/TasmanianDevilService"); describe("createTasmanianDevil", () => { let req = getMockReq(); const { res, mockClear } = getMockRes(); beforeEach(() => { TDS.createTasmanianDevilAsync = jest.fn().mockReturnValue(0); mockClear(); }); test("should validate attribute must be number", async () => { req.body.attribute = "not a number" await TDC.createTasmanianDevil(req, res); expect(res.status).toHaveBeenCalledWith(400); }); });
问题原因
直接调用控制器方法时,express-validator的校验中间件并没有被执行——这些校验规则是挂载在路由上的,单独调用控制器函数时,不会自动触发校验逻辑,所以req对象上没有被注入校验错误,validationResult(req)自然返回空。
解决方案
方案1:手动模拟validationResult的返回值
如果只是想测试控制器对校验错误的处理逻辑,可以直接MockvalidationResult函数,让它返回预设的错误结果:
import TDC from "../../controllers/TasmanianDevilController"; import TDS from "../../DomainLogic/TasmanianDevilService"; import { getMockReq, getMockRes } from "@jest-mock/express"; import { validationResult } from "express-validator"; jest.mock("../../DomainLogic/TasmanianDevilService"); // Mock validationResult jest.mock("express-validator", () => ({ ...jest.requireActual("express-validator"), validationResult: jest.fn(), })); describe("createTasmanianDevil", () => { let req = getMockReq(); const { res, mockClear } = getMockRes(); beforeEach(() => { TDS.createTasmanianDevilAsync = jest.fn().mockReturnValue(0); mockClear(); // 重置mock默认返回值 (validationResult as jest.Mock).mockReturnValue({ isEmpty: () => true, array: () => [], }); }); test("should return 400 when validation fails", async () => { // 让validationResult返回错误 (validationResult as jest.Mock).mockReturnValue({ isEmpty: () => false, array: () => [{ msg: "attribute must be an integer" }], }); await TDC.createTasmanianDevil(req, res); expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith([{ msg: "attribute must be an integer" }]); }); test("should proceed when validation passes", async () => { req.body.attribute = 123; await TDC.createTasmanianDevil(req, res); expect(TDS.createTasmanianDevilAsync).toHaveBeenCalledWith(123); expect(res.status).toHaveBeenCalledWith(200); }); });
方案2:手动执行校验链,模拟真实校验过程
如果想真实触发express-validator的校验逻辑,而非Mock结果,可以手动获取路由上的校验中间件,对req执行校验:
import TDC from "../../controllers/TasmanianDevilController"; import TDS from "../../DomainLogic/TasmanianDevilService"; import { getMockReq, getMockRes } from "@jest-mock/express"; import router from "../../routes/TasmanianDevilRouter"; // 引入路由文件 import { validationResult } from "express-validator"; jest.mock("../../DomainLogic/TasmanianDevilService"); describe("createTasmanianDevil", () => { let req = getMockReq(); const { res, mockClear } = getMockRes(); beforeEach(() => { TDS.createTasmanianDevilAsync = jest.fn().mockReturnValue(0); mockClear(); }); test("should validate attribute must be number", async () => { req.body.attribute = "not a number"; // 获取路由上的校验中间件数组 const routeLayer = router.stack.find( layer => layer.path === "/createTasmanianDevil" && layer.method === "post" ); const validationMiddlewares = (routeLayer.handle as any); // 依次执行所有校验中间件 await Promise.all(validationMiddlewares.map((mw: any) => mw(req, res, jest.fn()))); // 此时req已携带校验结果,调用控制器 await TDC.createTasmanianDevil(req, res); expect(res.status).toHaveBeenCalledWith(400); }); });
方案3:用Supertest测试完整路由
如果要测试整个请求流程(包括路由匹配、中间件执行、控制器处理),推荐用Supertest,它能模拟HTTP请求,自动触发所有路由中间件:
首先安装依赖:
npm install supertest --save-dev
编写测试代码:
import request from "supertest"; import express from "express"; import router from "../routes/TasmanianDevilRouter"; import TDS from "../DomainLogic/TasmanianDevilService"; jest.mock("../DomainLogic/TasmanianDevilService"); // 初始化Express应用 const app = express(); app.use(express.json()); app.use(router); describe("POST /createTasmanianDevil", () => { beforeEach(() => { TDS.createTasmanianDevilAsync = jest.fn().mockReturnValue(0); }); test("should return 400 when attribute is not an integer", async () => { const response = await request(app) .post("/createTasmanianDevil") .send({ attribute: "not a number" }); expect(response.status).toBe(400); expect(response.body).toEqual( expect.arrayContaining([expect.objectContaining({ msg: expect.stringContaining("must be an integer") })]) ); }); test("should return 200 when attribute is valid", async () => { const response = await request(app) .post("/createTasmanianDevil") .send({ attribute: 123 }); expect(response.status).toBe(200); expect(TDS.createTasmanianDevilAsync).toHaveBeenCalledWith(123); }); });
总结
- 若仅测试控制器对错误的处理逻辑,Mock
validationResult的方式最简单高效; - 若想验证校验规则本身是否生效,手动执行校验链或用Supertest测试完整路由更准确;
- Supertest适合集成测试场景,能覆盖从请求到响应的全流程,推荐用于验证路由与校验的组合逻辑。
内容的提问来源于stack exchange,提问作者Hactavius
相关产品推荐
相关产品推荐

