Vitest结合supertest测试Express时无法mock控制器的问题求助
两种写法的核心错误
spyOn写法失败原因
- mock时机滞后:调用
serve()初始化Express应用时,路由已经直接绑定了原始控制器函数的引用。后续通过vi.spyOn修改模块导出的属性,不会替换路由已经持有的原始函数引用,请求自然会走到原始逻辑。 - mock实现不完整:写的mock函数没有接收Express传入的
res对象、也没有返回响应,就算命中控制器也会导致请求挂起。
vi.mock整模块写法失败原因
- 初始化时机错误:在describe块顶层直接await执行
serve()初始化app,这个操作的执行时机早于mock生效的时间,路由加载的还是未被mock的原始控制器。 - mock函数写法错误:给控制器写的mock是
vi.fn(() => (req, res, next) => { res.status(201) }),相当于函数被调用时才返回一个中间件,而Express调用控制器是直接传入(req,res,next)三个参数,根本不会执行写的响应逻辑;加上只设置了状态码没有调用res.send()/res.json()结束请求,必然触发超时。
正确实现方案
Vitest的vi.mock会被自动提升到文件所有import语句之前执行,核心原则是:所有依赖mock模块的逻辑(包括app初始化、路由注册),必须在mock注册完成后再执行。
可直接运行的测试代码如下:
import request from "supertest"; import { describe, expect, test, vi, beforeAll, beforeEach } from "vitest"; import { serve } from "../../../server/__tests__/serve"; // mock鉴权中间件 vi.mock(`${process.cwd()}/server/middleware/verify-request.js`, () => ({ default: vi.fn(() => (req, res, next) => next()), })); // 提前定义mock控制器实例,方便后续断言调用情况 const mockAppMetafieldSave = vi.fn((req, res) => { res.status(201).json({ message: "mock save success" }); }); const mockAppMetafieldRetrieve = vi.fn((req, res) => { res.status(200).json({ mocked: true }); }); // mock控制器模块 vi.mock(`${process.cwd()}/server/controllers/metafieldsControllers.js`, () => ({ appMetafieldSave: mockAppMetafieldSave, appMetafieldRetrieve: mockAppMetafieldRetrieve, })); describe("metafields路由逻辑验证", () => { let app; // 所有mock注册完成后,再初始化app,保证路由加载的是mock后的控制器 beforeAll(async () => { const serveResult = await serve(process.cwd(), false); app = serveResult.app; }); // 每个用例执行前清空mock调用记录,避免用例间互相影响 beforeEach(() => { vi.clearAllMocks(); }); test("POST /metafields/save 请求可命中对应控制器", async () => { const res = await request(app).post("/metafields/save"); // 验证控制器被调用 expect(mockAppMetafieldSave).toHaveBeenCalledTimes(1); // 验证响应状态符合预期 expect(res.status).toBe(201); }); test("GET /metafields/:namespace/:key 请求可命中对应控制器", async () => { const res = await request(app).get("/metafields/test_namespace/test_key"); expect(mockAppMetafieldRetrieve).toHaveBeenCalledTimes(1); expect(res.status).toBe(200); }); });
额外说明
如果偏好spyOn的写法,需要在spy挂载完成后再动态导入路由模块、初始化app,参考逻辑如下:
// 示例:spyOn写法的正确时机 import * as metafieldsControllers from "../metafieldsControllers.js"; // 先挂载spy const saveSpy = vi.spyOn(metafieldsControllers, "appMetafieldSave").mockImplementation((req, res) => { res.status(201).json({}); }); // spy挂载完成后再初始化app const { app } = await serve(process.cwd(), false);
注意这种写法需要保证路由注册时拿到的是被spy包装后的函数引用,否则依然会失效。
内容的提问来源于stack exchange,提问作者dopla-dev
相关产品推荐
相关产品推荐

