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

Angular 16中Effect内信号与服务的单元测试问题排查

排查Angular单元测试中popover.close()未被调用的问题

组件核心逻辑

在Angular 16.2.12项目中,组件的ngAfterViewInit钩子内根据notificacoesView状态创建响应式effect,监听notificacoesService.notificacoes(数组类型Signal)的变化,处理通知图标状态并在通知为空时关闭popover:

protected iconNotificacaoNova = false;
@ViewChild('popoverNotificacao') popoverNotificacaoComponent!: NgbPopover;
@Input() public notificacoesView: boolean = false;
protected readonly notificacoesService = inject(NotificacoesService);

...

ngAfterViewInit(): void {
  if (this.notificacoesView) {
    effect(() => {
      if (this.popoverNotificacaoComponent?.isOpen() && this.notificacoesService.notificacoes().length) {
        this.iconNotificacaoNova = false;
      } else if (this.notificacoesService.notificacoes().length) {
        this.iconNotificacaoNova = true;
      }
      if (!this.notificacoesService.notificacoes().length) {
        this.popoverNotificacaoComponent.close();
      }
      this.cdr.detectChanges();
    }, { injector: this.injector });
  }
}

单元测试代码

编写的测试期望当notificacoes为空数组时,触发popoverNotificacaoComponent.close(),但测试失败,提示Expected spy NgbPopover.close to have been called:

describe('NavMenuComponent NgAfterViewInit', () => {
    let component: NavMenuComponent;
    let fixture: ComponentFixture<NavMenuComponent>;

    let mockActivatedRoute: ActivatedRoute;
    let notificacoesServiceMock: jasmine.SpyObj<NotificacoesService>;
    let popoverNotificacaoComponentMock: jasmine.SpyObj<NgbPopover>;


    it("must call the instance's close method popoverNotificationComponent", async () => {
        notificacoesServiceMock = jasmine.createSpyObj('NotificacoesService', ['notificacoes', 'updateNotificacoes']);
        popoverNotificacaoComponentMock = jasmine.createSpyObj('NgbPopover', ['isOpen', 'close']);

        await TestBed.configureTestingModule({
            imports: [
                NavMenuComponent,
                NgbPopoverModule,
            ],
            providers: [
                { provide: ActivatedRoute, useValue: mockActivatedRoute },
                { provide: NotificacoesService, useValue: notificacoesServiceMock },
            ]
        }).compileComponents();
        fixture = TestBed.createComponent(NavMenuComponent);
        component = fixture.componentInstance;

        popoverNotificacaoComponentMock.isOpen.and.returnValue(true);
        popoverNotificacaoComponentMock.close.and.returnValue();
        component.popoverNotificacaoComponent = popoverNotificacaoComponentMock;

        const spyCDR = spyOn((component as any).cdr, 'detectChanges');

        component.notificacoesView = true;

        notificacoesServiceMock.notificacoes.and.returnValue([]);

        fixture.detectChanges();

        expect((component as any).iconNotificacaoNova).toBeFalse();
        expect(spyCDR).toHaveBeenCalled();
        expect(popoverNotificacaoComponentMock.close).toHaveBeenCalled();
    });
});

问题根源与解决方法

1. Signal模拟方式错误

notificacoesService.notificacoes是Angular Signal类型,而非普通方法。原测试用jasmine.createSpyObj将其模拟为普通方法spy,导致effect无法监听Signal的响应式变化,effect内的逻辑根本没有执行。

修正方式:
导入signal并手动给mock添加Signal类型的notificacoes属性:

import { signal } from '@angular/core';

// ...

// 创建mock时不包含notificacoes,后续手动设置为Signal
notificacoesServiceMock = jasmine.createSpyObj('NotificacoesService', ['updateNotificacoes']);
// 给mock添加Signal类型的notificacoes属性,初始值为空数组
notificacoesServiceMock.notificacoes = signal([]);

2. 移除无效的spy返回值设置

原测试中notificacoesServiceMock.notificacoes.and.returnValue([])是无效的,因为现在notificacoes是Signal而非方法,直接删除该行代码即可。

3. 确保effect触发执行

当Signal初始值为空数组时,effect在创建时会自动执行一次。修改后的测试中,fixture.detectChanges()触发ngAfterViewInit创建effect,此时会读取Signal的空数组值,触发popover.close()的逻辑。

修正后的完整测试代码

import { signal } from '@angular/core';
// ...其他必要导入

describe('NavMenuComponent NgAfterViewInit', () => {
    let component: NavMenuComponent;
    let fixture: ComponentFixture<NavMenuComponent>;

    let mockActivatedRoute: ActivatedRoute;
    let notificacoesServiceMock: jasmine.SpyObj<NotificacoesService>;
    let popoverNotificacaoComponentMock: jasmine.SpyObj<NgbPopover>;


    it("must call the instance's close method popoverNotificationComponent", async () => {
        notificacoesServiceMock = jasmine.createSpyObj('NotificacoesService', ['updateNotificacoes']);
        // 手动设置Signal类型的notificacoes属性
        notificacoesServiceMock.notificacoes = signal([]);
        
        popoverNotificacaoComponentMock = jasmine.createSpyObj('NgbPopover', ['isOpen', 'close']);

        await TestBed.configureTestingModule({
            imports: [
                NavMenuComponent,
                NgbPopoverModule,
            ],
            providers: [
                { provide: ActivatedRoute, useValue: mockActivatedRoute },
                { provide: NotificacoesService, useValue: notificacoesServiceMock },
            ]
        }).compileComponents();
        
        fixture = TestBed.createComponent(NavMenuComponent);
        component = fixture.componentInstance;

        popoverNotificacaoComponentMock.isOpen.and.returnValue(true);
        popoverNotificacaoComponentMock.close.and.returnValue();
        component.popoverNotificacaoComponent = popoverNotificacaoComponentMock;

        const spyCDR = spyOn((component as any).cdr, 'detectChanges');

        component.notificacoesView = true;

        fixture.detectChanges(); // 触发生命周期钩子与effect执行

        expect((component as any).iconNotificacaoNova).toBeFalse();
        expect(spyCDR).toHaveBeenCalled();
        expect(popoverNotificacaoComponentMock.close).toHaveBeenCalled();
    });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 20:55:54