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

如何对依赖其他异步调用的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:01:55