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

Cypress Spy未触发:Angular组件Mock服务方法断言失败排查

问题:Angular组件测试中Mock服务方法调用断言失败

当选择无效文件类型时,EditProfileCardComponent会调用messageHandlingService.displayMessage方法,但Cypress测试中第二个断言失败,提示该方法从未被调用,第一个断言(验证test值更新)已通过。


组件TypeScript代码

export class EditProfileCardComponent {

  test = 'test'

  constructor(private messageHandlingService: MessageHandlingService) {}

  onFileSelected(event: any) {  
    const file: File = event.target.files[0];
    if(file.type != ValidFileTypes.JPG && file.type != ValidFileTypes.PNG) {
      this.test = "passed"; // this correctly sets the value to 'passed'
      this.displayInvalidFileTypeMessage();
    } else {
      // Some other logic...
    }
  }

 private displayInvalidFileTypeMessage() {
    this.messageHandlingService.displayMessage({
      message: "File selected must be JPEG or PNG file",
      action: SnackbarAction.Close,
      classType: SnackbarClassType.Error,
      duration: SnackbarDuration.Medium
    });
  }

}

HTML代码

<p data-cy="test">{{test}}</p>
<input
   type="file"
   class="d-none"
   data-cy="file-input"
   accept=".png, .jpg"
   (change)="onFileSelected($event)"
   #fileUpload>

测试代码

const messageHandlingService = {
  displayMessage: (snackBarItem: SnackBarItem) => {
    return null;
  }
}

beforeEach(() => {
  cy.mount(EditProfileCardComponent, setupCypressConfig<EditProfileCardComponent>({
    providers: [
      {provide: MessageHandlingService, useValue: messageHandlingService}
    ]
  }));
});

it("onFileSelected method should call display message method in messageHandlingService when invalid file type is selected", () => {
  cy.spy(messageHandlingService, 'displayMessage');
  cy.get(elementBindings.fileInput).selectFile('cypress/fixtures/incorrect file type.webp', { force: true });
  cy.wait(1000); 

  //1st assertion (THIS PASSES)
  cy.get('[data-cy="test"]').should('have.text', 'passed');

  //2nd assertion (THIS FAILS)
  expect(messageHandlingService.displayMessage).to.be.calledOnceWithExactly({
    message: "File selected must be JPEG or PNG file",
    action: SnackbarAction.Close,
    classType: SnackbarClassType.Error,
    duration: SnackbarDuration.Medium
  });
});

错误信息

AssertionError: expected displayMessage to have been called exactly once with exact arguments Object{4}, but it was never called

实际MessageHandlingService代码

import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subject, tap } from 'rxjs';
import { SnackBarItem } from '../models/snackbar-item';

@Injectable({
  providedIn: 'root'
})

export class MessageHandlingService {

  constructor(private snackbar: MatSnackBar) { }

  private displayNewMessage = new Subject<SnackBarItem>();

  newMessageReceived$ = this.displayNewMessage.pipe(
    tap(snackBarItem => this.displayMessage(snackBarItem))
  ).subscribe();

  onDisplayNewMessage(snackBarItem: SnackBarItem) {
    this.displayNewMessage.next(snackBarItem);
  }

  displayMessage(snackBarItem: SnackBarItem) {    
    this.snackbar.open(snackBarItem.message, snackBarItem.action, {
      panelClass: [snackBarItem.classType],
      duration: snackBarItem.duration,
      horizontalPosition: 'end'
    });
  }
}

原因分析

问题出在Spy创建时机:
组件在beforeEach中已经完成挂载,此时组件内部已经持有了messageHandlingService.displayMessage方法的原始引用。之后在测试用例中调用cy.spy只是修改了外部对象的方法,但组件仍然会调用初始化时拿到的原始方法,导致Spy无法捕获到调用。


解决方案

需要在挂载组件之前创建Spy,确保组件注入的是已经被Spy包装后的服务方法。修改测试代码如下:

beforeEach(() => {
  // 在挂载前创建带Spy的服务对象
  const messageHandlingService = {
    displayMessage: cy.spy().as('displayMessage') // 创建命名Spy
  };

  cy.mount(EditProfileCardComponent, setupCypressConfig<EditProfileCardComponent>({
    providers: [
      {provide: MessageHandlingService, useValue: messageHandlingService}
    ]
  }));
});

it("onFileSelected method should call display message method in messageHandlingService when invalid file type is selected", () => {
  cy.get(elementBindings.fileInput).selectFile('cypress/fixtures/incorrect file type.webp', { force: true });

  // 第一个断言
  cy.get('[data-cy="test"]').should('have.text', 'passed');

  // 第二个断言:使用命名Spy进行验证
  cy.get('@displayMessage').should('have.been.calledOnceWithExactly', {
    message: "File selected must be JPEG or PNG file",
    action: SnackbarAction.Close,
    classType: SnackbarClassType.Error,
    duration: SnackbarDuration.Medium
  });
});

关键修改点:

  1. 将服务对象的创建和Spy定义移到beforeEach内部,确保每次测试使用全新的Spy实例。
  2. 直接用cy.spy()定义服务的displayMessage方法,组件挂载时注入的就是已被Spy包装后的方法。
  3. 使用Cypress命名Spy(.as('displayMessage')),通过cy.get('@displayMessage')断言,符合Cypress异步测试逻辑,可移除手动cy.wait(1000)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 17:34:53