如何对依赖其他异步调用的Node.js异步函数进行单元测试?
如何用Jest+Sinon测试包含多异步调用的Mongoose服务方法
刚接触异步单元测试确实容易懵,不过咱们把这个problemFunc拆解成几个逻辑块,逐个mock和测试就清晰了。核心思路是:单元测试只关注当前Service的逻辑,所有外部依赖(其他服务、数据库模型)都要被mock掉,不让真实的数据库调用或外部服务影响测试结果。
第一步:准备测试环境,Mock所有外部依赖
首先,你需要把SomeOtherService1、SomeOtherService2、CalculationService这些静态服务,以及传入的dbModel实例方法全部mock。这里结合Jest和Sinon来实现:
1. Mock静态服务方法
用Jest的jest.mock来模拟这些服务的静态方法:
import Service from './Service'; import SomeOtherService1 from './SomeOtherService1'; import SomeOtherService2 from './SomeOtherService2'; import CalculationService from './CalculationService'; import sinon from 'sinon'; // Mock静态服务,替换成jest mock函数 jest.mock('./SomeOtherService1', () => ({ getData: jest.fn() })); jest.mock('./SomeOtherService2', () => ({ getData: jest.fn() })); jest.mock('./CalculationService', () => ({ calc: jest.fn() }));
2. Mock Mongoose模型实例
因为Service的构造函数接收dbModel,所以我们创建一个mock的模型对象,用Sinon stub它的链式调用方法(比如findById、where、exists):
describe('Service.problemFunc', () => { let service: Service; let mockDbModel: any; beforeEach(() => { // 初始化mock模型,处理Mongoose的链式调用 const whereStub = sinon.stub().returnsThis(); const existsStub = sinon.stub(); mockDbModel = { findById: sinon.stub().returns({ where: whereStub, exists: existsStub }), findByIdAndUpdate: sinon.stub() }; service = new Service(mockDbModel); // 重置所有mock的调用记录和状态,避免测试间互相干扰 jest.clearAllMocks(); sinon.reset(); }); });
这里要注意Mongoose的Query链式特性:findById返回的是Query对象,所以我们让stub返回带有where和exists方法的对象,where还要返回自身(returnsThis())才能继续链式调用。
第二步:分场景编写测试用例
现在我们覆盖所有关键逻辑分支,每个场景对应一个测试用例:
场景1:SomeOtherService1调用失败,返回错误
it('should return error when SomeOtherService1.getData fails', async () => { // 模拟服务调用抛出错误 const testError = new Error('Service1 failed'); (SomeOtherService1.getData as jest.Mock).mockRejectedValue(testError); // 调用目标方法 const result = await service.problemFunc('arg1', 'arg2', 'arg3'); // 断言结果是错误对象,且后续服务/数据库调用未执行 expect(result).toEqual(testError); expect(SomeOtherService2.getData).not.toHaveBeenCalled(); expect(mockDbModel.findById).not.toHaveBeenCalled(); });
场景2:数据库查询失败,返回错误
it('should return error when db query fails', async () => { // 模拟前两个服务调用成功 (SomeOtherService1.getData as jest.Mock).mockResolvedValue('mockData1'); (SomeOtherService2.getData as jest.Mock).mockResolvedValue('mockData2'); // 模拟数据库查询抛出错误 const dbError = new Error('DB query failed'); mockDbModel.findById().exists.rejects(dbError); const result = await service.problemFunc('arg1', 'arg2', 'arg3'); expect(result).toEqual(dbError); expect(CalculationService.calc).not.toHaveBeenCalled(); });
场景3:计算成功,执行更新并返回更新后的数据
it('should update database and return updated data when calculation succeeds', async () => { // 模拟所有依赖调用成功 const mockData1 = { id: 'data1' }; const mockData2 = { id: 'data2' }; const mockData3 = { _id: 'arg1', some_field: undefined }; const mockCalcResult = { some_field: 'newValue' }; const mockUpdatedData = { _id: 'arg1', ...mockCalcResult }; (SomeOtherService1.getData as jest.Mock).mockResolvedValue(mockData1); (SomeOtherService2.getData as jest.Mock).mockResolvedValue(mockData2); mockDbModel.findById().exists.resolves(mockData3); (CalculationService.calc as jest.Mock).mockReturnValue(mockCalcResult); mockDbModel.findByIdAndUpdate.resolves(mockUpdatedData); mockDbModel.findById.resolves(mockUpdatedData); const result = await service.problemFunc('arg1', 'arg2', 'arg3'); // 断言计算方法被正确调用 expect(CalculationService.calc).toHaveBeenCalledWith('arg1', mockData1, mockData2, mockData3); // 断言数据库更新方法被调用 expect(mockDbModel.findByIdAndUpdate).toHaveBeenCalledWith('arg1', mockCalcResult); // 断言返回的是更新后的数据 expect(result).toEqual(mockUpdatedData); });
场景4:计算返回false,返回错误
it('should return error when calculation returns false', async () => { // 模拟前序调用成功,但计算返回false (SomeOtherService1.getData as jest.Mock).mockResolvedValue('data1'); (SomeOtherService2.getData as jest.Mock).mockResolvedValue('data2'); mockDbModel.findById().exists.resolves('data3'); (CalculationService.calc as jest.Mock).mockReturnValue(false); const result = await service.problemFunc('arg1', 'arg2', 'arg3'); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Fail'); // 断言数据库更新未执行 expect(mockDbModel.findByIdAndUpdate).not.toHaveBeenCalled(); });
几个关键注意点
- 异步测试必须用async/await:Jest会自动等待Promise完成,避免测试提前结束导致断言失效。
- mock要重置:每个测试前清空mock的调用记录,保证测试独立性。
- 链式调用的处理:Mongoose的Query方法需要模拟返回自身,才能保证链式调用能正常执行。
- 覆盖所有分支:要确保每个try/catch、if/else分支都被测试覆盖,这样才能验证代码的稳定性。
内容的提问来源于stack exchange,提问作者VRN.dev
相关产品推荐
相关产品推荐

