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

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();
    });

});

问题原因分析

  1. 错误块执行两次:测试用例中手动调用并订阅了commandService.commandsGet,同时调用component.ngOnInit后,组件内部又执行了一次commandsGet的订阅,导致两次触发错误回调。第二次回调时,组件可能已处于不稳定状态,导致TranslateService调用返回undefined。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 14:07:06