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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 05:44:52