
在 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()”。这是官方推荐、稳定高效、符合测试隔离原则的最佳实践。