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

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']);
});

关键修改点

  1. 将测试函数改为async,并await组件的uploadFile方法,确保异步流程全部完成后再执行断言
  2. 调整pdfSummarizerService.summarizePdf的返回值为包含summary属性的对象,匹配组件代码的逻辑

内容的提问来源于stack exchange,提问作者ReVulV agina

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 00:19:44