Angular单元测试subscribe内嵌套服务调用未触发排查
错误原因定位
测试不通过由两类问题导致,核心问题为实例引用错误:
- 代码笔误:给service1打桩返回值时写的
of([]])多了1个多余的右方括号,若实际代码存在该问题会导致Observable创建逻辑异常,无法正常触发next回调,你提到lint无报错大概率是粘贴代码时的手滑。 - 核心逻辑错误(这类Angular单元测试最高发的坑):你创建
spyService2时监听的Service2实例,和组件实际注入使用的实例不是同一个。
该问题通常出现在组件@Component装饰器的providers数组中单独声明了Service2的场景:Angular会为该组件创建独立的Service2实例,而你在测试中通过TestBed.inject(Service2)拿到的是根注入器的全局实例,给全局实例打spy自然无法监听到组件内部私有实例的方法调用,完全匹配报错里Number of calls: 0的现象——你监听的对象从头到尾就没被调用过。
这也能解释为什么expect(spyService1).toHaveBeenCalled()断言可以正常通过:Service1是根注入器提供的单例,你spyOn的实例和组件使用的是同一个,所以能正常监听到调用。
修复方案
- 修正笔误,删除多余的右方括号,service1的spy返回值写成
of([])即可。 - 不要从TestBed拿Service2实例打桩,直接从组件实例上获取它持有的Service2引用创建spy,彻底避开实例不一致问题。TS的私有属性编译后依然会挂载在实例上,可以用方括号语法绕过TS的访问限制:
const spyService2 = spyOn(component['service2'], 'service2Method');
- 补充说明:
of()创建的是同步Observable,在调用component.myFunction(1)的瞬间就会同步执行完next回调,实际上不需要调用tick(),保留也不会影响结果。
修复后的完整测试代码参考:
it('should call 2nd service method upon 1st service method success', fakeAsync(() => { // 修正多余括号的笔误 const spyService1 = spyOn(service1, 'service1Method').and.returnValue( of([]) ); // 直接从组件实例获取持有的service2打桩,避免实例不一致 const spyService2 = spyOn(component['service2'], 'service2Method'); component.myFunction(1); tick(); expect(spyService1).toHaveBeenCalled(); expect(spyService2).toHaveBeenCalledWith({ param1: true, param2: false }); }));
排查小技巧
如果修复后依然存在问题,可以在subscribe的next回调里加临时打印,同时打印component['service2'] === service2验证实例一致性:
(response) => { console.log('next回调触发,service2实例是否一致:', this.service2 === 外部测试拿到的service2实例); this.service2.service2Method({ param1: true, param2: false }); }
如果控制台能打印出next回调触发的日志,但spy依然显示0次调用,可100%确认是spy绑定的服务实例和组件实际使用的实例不一致。
内容的提问来源于stack exchange,提问作者Abhishek Tirkey
相关产品推荐
相关产品推荐

