Jest 中为每个 it() 测试用例配置独立的模块 mock 实现

4次阅读

Jest 中为每个 it() 测试用例配置独立的模块 mock 实现

在 jest 中,`jest.mock()` 无法在 `it()` 内部动态生效;正确做法是顶层调用 `jest.mock()` 启用自动模拟,再结合 `jest.spyon()` + `mockimplementation()` 在每个测试中覆盖具体行为,并通过 `aftereach` 清理状态,确保测试隔离。

要为每个 it() 测试用例提供完全独立、互不干扰的模块 mock 实现(例如不同行为的中间件),关键在于理解 Jest 模块模拟的生命周期和隔离机制。直接在 it() 内调用 jest.mock() 是无效的——Jest 仅在模块加载时(即文件顶部)处理 jest.mock() 调用,运行时调用会被忽略。

✅ 正确且推荐的三步策略如下:

1. 顶层 jest.mock() 启用自动模拟

在文件顶部(describe 外或 describe 内顶部)调用 jest.mock(),启用对目标模块的自动模拟(auto-mock)。这会生成一个带默认 jest.fn() 行为的模拟模块,但不定义具体实现

// test.js —— 文件顶部 jest.mock('../../../middleware/awsTransferMiddleware');  const awsTransferMiddleware = require('../../../middleware/awsTransferMiddleware');

⚠️ 注意:必须 require()(而非 import)该模块,否则 jest.spyOn() 将无法访问其属性(ESM 静态导入在 Jest 中不支持运行时重写)。

2. 使用 jest.spyOn() 动态覆盖方法实现

在每个 it() 中,使用 jest.spyOn(对象, 方法名) 获取对模拟函数的引用,并调用 .mockImplementation() 设置当前测试专属逻辑

it("Should return 200 if at least one image exists", async () => {   // ✅ 覆盖 transferS3Files:模拟成功上传   jest.spyOn(awsTransferMiddleware, 'transferS3Files')     .mockImplementation(async (req, res, next) => {       req.s3TransferResult = { success: true, count: 2 };       next();     });    // ✅ 覆盖 filterPassedImage:模拟筛选出 1 张有效图   jest.spyOn(awsTransferMiddleware, 'filterPassedImage')     .mockImplementation(async (req, res, next) => {       req.filteredImages = ['img1.jpg'];       next();     });    const response = await request(app).post('/upload');   expect(response.status).toBe(200); });
it("Should return 400 when S3 upload fails", async () => {   // ❌ 完全不同的行为:模拟 transferS3Files 抛错   jest.spyOn(awsTransferMiddleware, 'transferS3Files')     .mockImplementation(async (req, res, next) => {       const err = new Error('S3 timeout');       err.status = 503;       next(err);     });    // ✅ filterPassedImage 不执行(因 transferS3Files 已调用 next(err))   jest.spyOn(awsTransferMiddleware, 'filterPassedImage')     .mockImplementation(() => {       throw new Error('This should not be called');     });    const response = await request(app).post('/upload');   expect(response.status).toBe(400); // 假设错误被全局 handler 转换为 400 });

3. afterEach 全面清理,保障测试纯净性

在每个测试结束后,清除所有 mock 状态,防止“泄漏”到下一个用例:

afterEach(() => {   jest.restoreAllMocks(); // 恢复所有被 spy 的原始方法(如需)   jest.clearAllMocks();   // 清空所有 mock 函数的调用记录与返回值 });

✅ jest.clearAllMocks() 是核心:它重置 mockImplementation、mockReturnValue、调用计数等,使下个 it() 可安全重新设置。

? 为什么不用 jest.resetModules()?

jest.resetModules() 会重新 require 模块,但对已 require 的模块实例(如 app 或 request 中依赖的中间件)无效,且可能破坏 supertest 的应用实例缓存。而 spyOn + clearAllMocks 更精准、轻量、可靠。

✅ 最终完整结构示例

// test.js const request = require('supertest'); const app = require('../../../path/to/your/app');  // Step 1: Top-level mock jest.mock('../../../middleware/awsTransferMiddleware'); const awsTransferMiddleware = require('../../../middleware/awsTransferMiddleware');  // Step 2: Cleanup after each test afterEach(() => {   jest.restoreAllMocks();   jest.clearAllMocks(); });  describe('POST /upload', () => {   it('returns 200 on successful transfer and filtering', async () => {     jest.spyOn(awsTransferMiddleware, 'transferS3Files')       .mockImplementation(async (req, res, next) => {         req.s3TransferResult = { success: true };         next();       });     jest.spyOn(awsTransferMiddleware, 'filterPassedImage')       .mockImplementation(async (req, res, next) => {         req.filteredImages = ['a.jpg'];         next();       });      const res = await request(app).post('/upload');     expect(res.status).toBe(200);   });    it('returns 500 when S3 transfer throws', async () => {     jest.spyOn(awsTransferMiddleware, 'transferS3Files')       .mockImplementation(async () => {         throw new Error('Network error');       });      const res = await request(app).post('/upload');     expect(res.status).toBe(500);   }); });

? 总结:Jest 的模块 mock 隔离不靠“多次 jest.mock()”,而靠 “一次模拟 + 多次 spyOn().mockImplementation() + 每次 clearAllMocks()”。这是官方推荐、稳定高效、符合测试隔离原则的最佳实践。

text=ZqhQzanResources