Angular 16中Effect内信号与服务的单元测试问题排查
组件核心逻辑
在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

