You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 06:18:09