Angular单元测试:Mock TranslationService时调用验证异常求助
Angular Karma单元测试问题:commandsGet返回400时错误块执行两次的排查与解决
问题场景
在测试Angular组件中commandService.commandsGet返回Bad Request(400)的逻辑时,发现错误回调块执行两次:第一次mock值符合预期,第二次返回undefined。同时,验证TranslateService.instant的调用时,spy提示服务未被正确调用。
组件目标代码
this._commandService.commandsGet(this.oem, this.countryCode).pipe(takeUntil(this._unsubscribeAll)).subscribe( response => { this.commands = Object.values(response.data); this.commandDropDownList(this.commands); }, () => { const errorMessage = true; const errorMessageContent = this._translateService.instant('CONTENT.There was a problem with the Command Service') + '. ' + this._translateService.instant('CONTENT.Please contact your administrator'); this._messageService.errorMessage(errorMessage, errorMessageContent); } );
现有测试代码
初始化配置
TestBed.configureTestingModule({ imports: [ HttpClientModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateFakeLoader } }), MatDialogModule, HttpClientTestingModule, TranslateTestingModule.withTranslations('en', en) ], declarations: [DialogSendcommandComponent, TranslatePipe], providers: [ CommandService, MessageService, { provide: MatDialogRef, useValue: mockMatDialog } ] }).compileComponents(); })); afterEach(() => { TestBed.resetTestingModule(); });
存在问题的测试用例
describe('Test Methods _commandService.commandsGet', () => { beforeEach(() => { TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: dataAml }); translateService = TestBed.inject(TranslateService); commandService = TestBed.inject(CommandService); httpMock = TestBed.inject(HttpTestingController); fixture = TestBed.createComponent(DialogSendcommandComponent); component = setComponent(formBuilder, component, fixture); }); afterEach(() => { httpMock.verify(); }) it('should display error message if commandsGet returns error', (done: DoneFn) => { spyOn(commandService, 'commandsGet').and.returnValue(throwError({ status: 400, error: 'Bad Request' })); const instantSpy = spyOn(translateService, 'instant').and.returnValues( 'Translated Text 1', 'Translated Text 2' ); commandService.commandsGet(dataAml.oem, dataAml.countryCode).subscribe({ next: () => { // This should not be called for a bad request fail('Expected error response'); }, error: (error) => { // Expecting an error response expect(error.status).toBe(400); expect(error.error).toBe('Bad Request'); expect(instantSpy).toHaveBeenCalledTimes(2); expect(instantSpy.calls.allArgs()).toEqual([ ['CONTENT.There was a problem with the Command Service'], ['CONTENT.Please contact your administrator'] ]); } }); component.ngOnInit(); fixture.detectChanges(); done(); }); });
问题原因分析
- 错误块执行两次:测试用例中手动调用并订阅了
commandService.commandsGet,同时调用component.ngOnInit后,组件内部又执行了一次commandsGet的订阅,导致两次触发错误回调。第二次回调时,组件可能已处于不稳定状态,导致TranslateService调用返回undefined。 - Spy验证失败:手动订阅的回调会先执行,但此时组件内部的订阅尚未触发,当组件内部订阅触发时,可能因为
takeUntil的_unsubscribeAll已经生效(或组件变更检测导致状态异常),使得TranslateService的调用不符合预期。
解决方法
1. 移除手动订阅
测试用例不需要自己调用commandsGet,ngOnInit会触发组件内部的订阅逻辑,只需关注组件行为的验证。
2. 调整验证时机
通过fakeAsync和tick控制异步流程,确保所有异步操作完成后再进行验证,避免因时序问题导致的验证失败。
3. 正确模拟服务依赖
确保_unsubscribeAll在测试中正确初始化(比如是一个Subject),避免订阅提前取消。
修正后的测试用例
it('should display error message if commandsGet returns error', fakeAsync(() => { // 模拟commandsGet返回错误 spyOn(commandService, 'commandsGet').and.returnValue(throwError({ status: 400, error: 'Bad Request' })); // 模拟TranslateService的instant方法 const instantSpy = spyOn(translateService, 'instant').and.returnValues( 'Translated Text 1', 'Translated Text 2' ); // 同时监控MessageService,验证错误提示是否被调用 const errorMsgSpy = spyOn(messageService, 'errorMessage'); // 触发组件初始化 component.ngOnInit(); fixture.detectChanges(); // 推进异步操作完成 tick(); // 验证commandsGet被调用 expect(commandService.commandsGet).toHaveBeenCalledWith(dataAml.oem, dataAml.countryCode); // 验证TranslateService被调用两次,参数正确 expect(instantSpy).toHaveBeenCalledTimes(2); expect(instantSpy).toHaveBeenCalledWith('CONTENT.There was a problem with the Command Service'); expect(instantSpy).toHaveBeenCalledWith('CONTENT.Please contact your administrator'); // 验证错误提示方法被调用 expect(errorMsgSpy).toHaveBeenCalledWith(true, 'Translated Text 1. Translated Text 2'); }));
额外注意事项
- 确保
_unsubscribeAll在组件中初始化为new Subject<void>(),并在ngOnDestroy中调用complete()。 - 测试中若使用
takeUntil,可在afterEach中调用component.ngOnDestroy()清理订阅,避免影响其他测试用例。
内容的提问来源于stack exchange,提问作者Christèle Legeard
相关产品推荐
相关产品推荐

