Angular测试异常:Spy未检测到预期方法调用(附代码)
测试Angular组件uploadFile方法时服务调用验证失败的问题
问题背景
我在测试Angular组件的uploadFile方法,控制台能打印出setting summary这类日志,说明方法已执行,但始终检测不到对应服务方法的调用,抛出断言失败的错误。
组件的uploadFile方法代码
public async uploadFile() { console.log('uploadFile'); this.isGeneratingResponse = true; this.disableUpload = true; this.errorMessage = ''; const base64String = await this.pdfToBase64TransformerService.convertPdfToBase64(this.selectedFile!); console.log('base64String', base64String); const subscription = this .pdfSummarizerService .summarizePdf(base64String) .subscribe({ next: (response) => { console.log('>>> test', response); this.isGeneratingResponse = false; console.log('setting summary'); this.summaryService.setSummary(response.summary); console.log('refreshing user credits'); this.userCreditsService.refreshUserCredits(); this.router.navigate(['/summary']); }, error: (error) => { this.errorMessage = error.error.message; this.disableUpload = false; this.isGeneratingResponse = false; } }); this.subscriptions.push(subscription); }
测试代码(使用ngneat/spectator)
let spectator: Spectator<DashboardComponent>; const userInfo = { id: '1234', username: 'Cypher', dateCreated: new Date().toISOString(), emailAddress: 'Cypher@gmail.com' } as UserInfo; const newUserCredits = { userId: '12345', maxCredits: 1, creditsUsed: 0, creditsRemaining: 1, } as UserCredits; const createComponent = createComponentFactory({ component: DashboardComponent, providers: [ mockProvider(PdfToBase64TransformerService), mockProvider(PdfSummarizerService), mockProvider(UserCreditsService, { userCredits$: of(newUserCredits) }), mockProvider(SummaryService), mockProvider(Router), mockProvider(UserInfoService, { userInfo$: of(userInfo) }) ], declarations: [ MockComponents(CreditInfoComponent) ] }); // other code here. it('should upload the file when clicked and redirect to the summary page.', () => { spectator = createComponent(); const file = new File([''], 'file1.pdf', { type: 'application/pdf' }); spectator.component.selectedFile = file; const pdfToBase64TransformerService = spectator.inject(PdfToBase64TransformerService); pdfToBase64TransformerService.convertPdfToBase64.and.returnValue(Promise.resolve('some base64')); const pdfSummarizerService = spectator.inject(PdfSummarizerService); pdfSummarizerService.summarizePdf.and.returnValue(of('some summary')); const summaryService = spectator.inject(SummaryService); const router = spectator.inject(Router); const userCreditsService = spectator.inject(UserCreditsService); spectator.component.uploadFile(); spectator.detectChanges(); // 这些断言失败 expect(userCreditsService.refreshUserCredits).toHaveBeenCalled(); expect(summaryService.setSummary).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/summary']); });
错误信息
Expected spy refreshUserCredits to have been called. Expected spy setSummary to have been called. Expected spy navigate to have been called with: [ [ '/summary' ] ] but it was never called.
问题原因与修复方案
核心问题
uploadFile是异步方法,你调用后立刻执行断言,但此时内部的await操作和Observable的next回调还未执行完成,断言时机过早。另外,你返回的of('some summary')是字符串,和组件代码中期望的response.summary结构不匹配(组件需要一个带summary属性的对象)。
修复后的测试代码
it('should upload the file when clicked and redirect to the summary page.', async () => { spectator = createComponent(); const file = new File([''], 'file1.pdf', { type: 'application/pdf' }); spectator.component.selectedFile = file; const pdfToBase64TransformerService = spectator.inject(PdfToBase64TransformerService); pdfToBase64TransformerService.convertPdfToBase64.and.returnValue(Promise.resolve('some base64')); const pdfSummarizerService = spectator.inject(PdfSummarizerService); // 返回带summary属性的对象,匹配组件代码中的response结构 pdfSummarizerService.summarizePdf.and.returnValue(of({ summary: 'some summary' })); const summaryService = spectator.inject(SummaryService); const router = spectator.inject(Router); const userCreditsService = spectator.inject(UserCreditsService); // 等待异步方法完全执行完毕 await spectator.component.uploadFile(); expect(userCreditsService.refreshUserCredits).toHaveBeenCalled(); expect(summaryService.setSummary).toHaveBeenCalledWith('some summary'); expect(router.navigate).toHaveBeenCalledWith(['/summary']); });
关键修改点
- 将测试函数改为
async,并await组件的uploadFile方法,确保异步流程全部完成后再执行断言 - 调整
pdfSummarizerService.summarizePdf的返回值为包含summary属性的对象,匹配组件代码的逻辑
内容的提问来源于stack exchange,提问作者ReVulV agina
相关产品推荐
相关产品推荐

