Angular 19导航栏组件logout集成测试失败,请求排查
问题概述
导航栏组件在未登录时显示Login和Signup链接,登录时显示My Account和Logout链接。编写集成测试验证点击Logout链接是否调用logout方法,但测试失败,浏览器中功能正常。
组件代码
navbar.component.html
<nav class="navbar"> <a routerLink="/" class="logo">E-Market</a> <ul class="nav-links"> <li><a routerLink="/products">Products</a></li> <li id="login" *ngIf="!isLoggedIn"><a routerLink="/auth/login">Login</a></li> <li id="sign-up" *ngIf="!isLoggedIn"><a routerLink="/auth/sign-up">Sign up</a></li> <li id="account" *ngIf="isLoggedIn"><a routerLink="/account">My account</a></li> <li id="logout" *ngIf="isLoggedIn"><a (click)="logout()">Logout</a></li> </ul> </nav>
navbar.component.ts
import { Component, effect } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from 'src/app/core/services/auth.service'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', styleUrls: ['./navbar.component.scss'], standalone: false }) export class NavbarComponent { isLoggedIn!: boolean; constructor(private authService: AuthService) { effect(() => { this.isLoggedIn = this.authService.isLoggedIn(); }); } logout() { this.authService.logout(); } }
测试代码(原错误版本)
import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; import { NavbarComponent } from './navbar.component'; import { AuthService } from 'src/app/core/services/auth.service'; import { Router } from '@angular/router'; describe('NavbarComponent', () => { let component: NavbarComponent; let fixture: ComponentFixture<NavbarComponent>; let authServiceSpy: jasmine.SpyObj<AuthService>; let routerSpy: jasmine.SpyObj<Router>; beforeEach(() => { authServiceSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn', 'logout']); routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']); TestBed.configureTestingModule({ declarations: [NavbarComponent], providers: [ { provide: AuthService, useValue: authServiceSpy }, { provide: Router, useValue: routerSpy }, ], }).compileComponents(); fixture = TestBed.createComponent(NavbarComponent); component = fixture.componentInstance; }); it('should call logout method when clicking on "Logout"', () => { authServiceSpy.isLoggedIn.and.returnValue(true); fixture.detectChanges(); component = fixture.componentInstance; spyOn(component, 'logout').and.callThrough();; const logoutLink = fixture.nativeElement.querySelector('#logout'); logoutLink.click(); expect(component.logout).toHaveBeenCalled(); expect(authServiceSpy.logout).toHaveBeenCalled(); expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/'); }); });
测试错误信息
Chrome 131.0.0.0 (Windows 10) NavbarComponent should call logout method when clicking on "Logout" FAILED
Expected spy logout to have been called.
at
at UserContext.apply (src/app/navbar/components/navbar/navbar.component.spec.ts:67:30)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2082:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34)
Expected spy AuthService.logout to have been called.
at
at UserContext.apply (src/app/navbar/components/navbar/navbar.component.spec.ts:68:35)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2082:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34)
Expected spy Router.navigateByUrl to have been called with:
[ '/' ]
but it was never called.
at
at UserContext.apply (src/app/navbar/components/navbar/navbar.component.spec.ts:69:37)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2082:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34)
问题分析与修复
错误点1:错误的路由跳转期望
组件的logout方法仅调用authService.logout(),未执行路由跳转逻辑,测试中错误添加了router.navigateByUrl的断言,这会导致该断言必然失败。
错误点2:Spy时机与组件初始化顺序错误
原测试中在fixture.detectChanges()(组件初始化)后才对component.logout设置Spy,此时模板已绑定原始的logout方法,点击事件触发的是原始方法而非Spy方法,导致断言component.logout被调用失败。
错误点3:冗余的组件实例赋值
component = fixture.componentInstance;在beforeEach中已完成赋值,测试中重复赋值无意义,应删除。
修复后的测试代码
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NavbarComponent } from './navbar.component'; import { AuthService } from 'src/app/core/services/auth.service'; import { Router } from '@angular/router'; describe('NavbarComponent', () => { let component: NavbarComponent; let fixture: ComponentFixture<NavbarComponent>; let authServiceSpy: jasmine.SpyObj<AuthService>; beforeEach(() => { authServiceSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn', 'logout']); TestBed.configureTestingModule({ declarations: [NavbarComponent], providers: [ { provide: AuthService, useValue: authServiceSpy }, { provide: Router, useValue: jasmine.createSpyObj('Router', ['navigateByUrl']) }, ], }).compileComponents(); fixture = TestBed.createComponent(NavbarComponent); component = fixture.componentInstance; }); it('should call logout method when clicking on "Logout"', () => { // 1. 设置登录状态为已登录 authServiceSpy.isLoggedIn.and.returnValue(true); // 2. 在组件初始化前,对logout方法设置Spy spyOn(component, 'logout').and.callThrough(); // 3. 初始化组件,触发变更检测与effect执行 fixture.detectChanges(); // 4. 确认Logout链接存在 const logoutLink = fixture.nativeElement.querySelector('#logout'); expect(logoutLink).toBeTruthy('Logout链接未渲染,检查isLoggedIn状态是否正确'); // 5. 点击Logout链接 logoutLink.click(); // 6. 触发变更检测,确保事件处理完成 fixture.detectChanges(); // 7. 断言 expect(component.logout).toHaveBeenCalled(); expect(authServiceSpy.logout).toHaveBeenCalled(); }); });
额外说明(若AuthService使用Signal)
如果AuthService.isLoggedIn()返回的是Angular Signal而非普通布尔值,测试中需返回Signal实例:
import { signal } from '@angular/core'; // 在beforeEach中修改spy返回值 authServiceSpy.isLoggedIn.and.returnValue(signal(true)); // 同时组件中的effect需正确读取Signal值: effect(() => { this.isLoggedIn = this.authService.isLoggedIn()(); // 调用Signal获取值 });
内容的提问来源于stack exchange,提问作者gallup gallup

