Vitest+TypeScript下多场景Mock Multer路由测试优化
无需计数器的Multer多场景Mock方案(Vitest + TypeScript + Express)
你当前通过计数器切换Multer的Mock场景(文件超限返回400、通用错误返回500),这种实现依赖测试执行顺序,既不优雅也容易引发用例污染问题。以下是两种通用改进方案,完全摆脱计数器依赖:
方案一:动态Mock + 工厂函数(推荐)
通过vi.doMock在每个测试用例中动态生成符合场景的Multer Mock,配合工厂函数复用Mock逻辑,每个用例完全独立:
import request from "supertest"; import express from "express"; import { vi, describe, it, expect } from "vitest"; // 通用Multer Mock工厂:根据配置生成对应错误场景的Mock function createMulterMock(errorConfig?: { code: string; message: string; fileSize?: number }) { return () => { const single = vi.fn(() => (req: any, res: any, next: any) => { req.file = { originalname: "test.csv", size: errorConfig?.fileSize ?? 600 * 1024, path: "uploads/test.csv", }; if (errorConfig) { const error: any = new Error(errorConfig.message); error.code = errorConfig.code; next(error); } else { next(); // 无错误场景留空 } }); const diskStorage = vi.fn(() => ({ destination: vi.fn((req, file, cb) => cb(null, "uploads")), filename: vi.fn((req, file, cb) => cb(null, file.originalname)), })); const multerMock = () => ({ single, diskStorage: diskStorage(), }); multerMock.diskStorage = diskStorage; return { default: multerMock, diskStorage, }; }; } describe("POST /import/customers", () => { it("should return 400 if file size exceeds limit (512KB)", async () => { // 动态Mock Multer,模拟文件超限场景 vi.doMock("multer", createMulterMock({ code: "LIMIT_FILE_SIZE", message: "File too large", fileSize: 600 * 1024, })); // 重新导入路由,确保使用最新的Mock实例 const { default: router } = await import("../routes/import.route"); const app = express(); app.use(express.json()); app.use("/import", router); const res = await request(app) .post("/import/customers") .attach("file", Buffer.from("Some content"), "test.csv"); expect(res.status).toBe(400); expect(res.body.msg).toBe("File too large. Maximum size is 512KB."); }); it("should return 500 for general file upload error", async () => { // 动态Mock Multer,模拟通用上传错误场景 vi.doMock("multer", createMulterMock({ code: "UNKNOWN_ERROR", message: "Unknown error", fileSize: 100, })); const { default: router } = await import("../routes/import.route"); const app = express(); app.use(express.json()); app.use("/import", router); const res = await request(app) .post("/import/customers") .attach("file", Buffer.from("Some content"), "test.csv"); expect(res.status).toBe(500); expect(res.body.msg).toBe("File upload error"); }); });
方案优势
- 测试用例完全解耦,不依赖执行顺序,新增场景只需添加新配置
- 工厂函数可复用,后续扩展其他上传场景(如文件格式错误)只需新增配置
- 动态Mock确保每个用例使用独立的Mock实例,避免交叉污染
方案二:修改Mock实例实现
提前全局Mock Multer,在每个测试用例中直接修改single方法的实现,适合路由初始化成本较高的场景:
import request from "supertest"; import express from "express"; import importRouter from "../routes/import.route"; import { vi, describe, it, expect, beforeEach } from "vitest"; import multer from "multer"; // 提前全局Mock Multer,获取Mock实例 vi.mock("multer", () => { const single = vi.fn(); const diskStorage = vi.fn(() => ({ destination: vi.fn((req, file, cb) => cb(null, "uploads")), filename: vi.fn((req, file, cb) => cb(null, file.originalname)), })); const multerMock = () => ({ single, diskStorage: diskStorage(), }); multerMock.diskStorage = diskStorage; return { default: multerMock, diskStorage, }; }); describe("POST /import/customers", () => { let app: express.Express; beforeEach(() => { app = express(); app.use(express.json()); app.use("/import", importRouter); // 重置single方法,避免用例间污染 (multer as any).default().single.mockReset(); }); it("should return 400 if file size exceeds limit (512KB)", async () => { // 给single方法设置当前场景的实现 (multer as any).default().single.mockImplementation(() => (req: any, res: any, next: any) => { req.file = { originalname: "test.csv", size: 600 * 1024, path: "uploads/test.csv", }; const error: any = new Error("File too large"); error.code = "LIMIT_FILE_SIZE"; next(error); } ); const res = await request(app) .post("/import/customers") .attach("file", Buffer.from("Some content"), "test.csv"); expect(res.status).toBe(400); expect(res.body.msg).toBe("File too large. Maximum size is 512KB."); }); it("should return 500 for general file upload error", async () => { (multer as any).default().single.mockImplementation(() => (req: any, res: any, next: any) => { req.file = { originalname: "test.csv", size: 100, path: "uploads/test.csv", }; const error: any = new Error("Unknown error"); error.code = "UNKNOWN_ERROR"; next(error); } ); const res = await request(app) .post("/import/customers") .attach("file", Buffer.from("Some content"), "test.csv"); expect(res.status).toBe(500); expect(res.body.msg).toBe("File upload error"); }); });
方案优势
- 无需重复导入路由,代码更简洁
- Mock实例复用,减少模块重新加载的开销
- 用例间通过
mockReset隔离,避免状态污染
内容的提问来源于stack exchange,提问作者Flo
相关产品推荐
相关产品推荐

