Jest如何mock非类模块中的私有成员实现单元测试
Jest 测试非类模块私有成员的实现方案
针对这种模块作用域下的私有变量、私有函数,不需要修改业务源码,用以下方案即可完成mock,完全隔离真实数据源:
方案1:jest.mock 全量模块mock(最通用,零业务侵入)
Jest的模块mock能力可以直接替换整个目标模块的实现,你可以在mock工厂里自定义私有变量、私有函数的逻辑,还能在测试用例中直接控制mock状态、断言函数调用。
这个方案有几个明显优势:
- 完全不需要修改业务仓库模块的代码,所有mock逻辑收敛在测试文件中
- 可以直接控制私有变量
products的初始状态,完全匹配测试用例的预期 - 可以对
loadProducts、saveProducts这类私有函数做调用断言,验证内部逻辑是否符合预期
测试文件代码示例:
import { read, add } from './productRepo'; // 定义mock层的状态,放在mock工厂外层,方便测试用例直接读写 let mockProducts = []; const mockLoadProducts = jest.fn(() => { // 模拟原loadProducts的初始化逻辑,默认构造5条测试数据匹配用例预期 if (mockProducts.length === 0) { mockProducts = Array(5).fill(null).map((_, idx) => ({ id: idx, name: `test_prod_${idx}` })); } }); const mockSaveProducts = jest.fn(); // 对目标仓库模块做mock,第二个参数传入工厂函数,返回和原模块签名一致的导出 jest.mock('./productRepo', () => { return { read: jest.fn(() => { mockLoadProducts(); return mockProducts; }), add: jest.fn((product) => { mockLoadProducts(); mockProducts.push(product); mockSaveProducts(); }) }; }); // 每个用例执行前重置mock状态,避免用例之间数据污染 beforeEach(() => { jest.clearAllMocks(); // 重置商品列表为初始5条数据的状态 mockProducts = Array(5).fill(null).map((_, idx) => ({ id: idx, name: `test_prod_${idx}` })); }); // 你原来写的测试用例可以直接运行,不需要修改 it('can read products', () => { expect(read().length).toBe(5); }); it('can add a product', () => { const oldNum = read().length; add({id:0, name:'test prod'}); expect(read().length).toBe(oldNum+1); }); // 还可以额外断言私有函数的调用逻辑,比如验证新增商品时确实触发了持久化 it('add action will trigger save operation', () => { add({id:100, name: 'new prod'}); expect(mockSaveProducts).toHaveBeenCalledTimes(1); });
方案2:拆分数据源依赖(更符合架构设计的长期方案)
如果可以做少量代码优化,建议把模块里直接调用数据源的逻辑抽成独立的公共模块,比如单独新建productDb.js存放数据库/外部服务的读写方法:
// productDb.js export function fetchProductsFromDb() { // 真实的数据源查询逻辑 } export function saveProductsToDb(products) { // 真实的数据源写入逻辑 }
原仓库模块改为引入这个公共模块的方法:
import { fetchProductsFromDb, saveProductsToDb } from './productDb.js'; let products; function loadProducts() { if (!products) products = fetchProductsFromDb(); } function saveProducts() { saveProductsToDb(products); } // 原有的read、add导出逻辑保持不变
这种情况下单元测试不需要mock整个仓库模块,只需要mock./productDb.js的数据源方法即可,测试粒度更细,也更符合单一职责的设计原则。
方案3:不推荐的应急hack方案
如果完全不能修改业务代码、也不想全量mock模块,可以借助Node模块加载的内部钩子获取模块作用域的私有变量,但这种方式依赖Jest和Node的底层实现,版本升级很容易出现兼容问题,非极端场景不要用。
内容的提问来源于stack exchange,提问作者RapPayne
相关产品推荐
相关产品推荐

