如何正确编写Angular组件路由参数校验的Jasmine单元测试?
问题:LoginComponent路由参数监听方法的单元测试修复
业务逻辑说明
LoginComponent中的listenToRouteParameters方法负责处理路由查询参数,逻辑如下:
- 从
ActivatedRoute的snapshot中获取state和code查询参数 - 若两个参数均存在:
- 调用
CookieService.getCookieValue(state)获取codeVerifier - 若codeVerifier存在:执行
CookieService.removeCookie(),再调用initiateTokenExchange(code, codeVerifier) - 若codeVerifier不存在:调用
refreshTokens()
- 调用
- 若参数不全:直接调用
refreshTokens()
LoginComponent核心代码
listenToRouteParameters(): void { const state = this.route.snapshot?.queryParamMap.get('state'); const code = this.route.snapshot?.queryParamMap.get('code'); if (state && code) { const codeVerifier = this.cookieService.getCookieValue(state); if (codeVerifier) { this.cookieService.removeCookie(); this.initiateTokenExchange(code, codeVerifier); } else { this.refreshTokens(); } } else { this.refreshTokens(); } }
原测试代码的问题
你写的测试无法正确验证逻辑,原因包括:
- 模拟
ActivatedRoute时用了params属性,但业务代码是从snapshot.queryParamMap读取参数,两者不匹配 CookieService未用Jasmine spy模拟,直接传入未初始化的变量,导致断言无法触发- 测试依赖订阅
queryParams,但业务代码是一次性读取snapshot,修改订阅源不会影响snapshot的值 - 未显式调用
listenToRouteParameters方法,组件初始化流程可能未触发该方法
修正后的单元测试实现
以下是覆盖所有场景的正确测试代码,每个用例对应一种业务分支:
let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let route: ActivatedRoute; // 用Jasmine spy模拟CookieService的所有方法 let cookieServiceSpy: jasmine.SpyObj<CookieService>; // 模拟ActivatedRoute的queryParamMap let queryParamMapSpy: jasmine.SpyObj<ParamMap>; beforeEach(async () => { // 创建CookieService的spy对象 cookieServiceSpy = jasmine.createSpyObj('CookieService', ['getCookieValue', 'removeCookie']); // 创建ParamMap的spy对象 queryParamMapSpy = jasmine.createSpyObj('ParamMap', ['get']); await TestBed.configureTestingModule({ declarations: [LoginComponent], imports: [ FormsModule, ReactiveFormsModule, HttpClientModule, RouterTestingModule ], providers: [ { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: queryParamMapSpy } } }, { provide: CookieService, useValue: cookieServiceSpy } ] }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; route = TestBed.inject(ActivatedRoute); // 初始化组件方法的spy,方便验证调用情况 spyOn(component, 'initiateTokenExchange'); spyOn(component, 'refreshTokens'); }); // 测试场景1:存在state和code,且有对应codeVerifier it('should remove cookie and exchange tokens when state, code and codeVerifier exist', () => { // 设置queryParamMap的get方法返回值 queryParamMapSpy.get.withArgs('state').and.returnValue('test-state'); queryParamMapSpy.get.withArgs('code').and.returnValue('test-code'); // 设置getCookieValue返回存在的codeVerifier cookieServiceSpy.getCookieValue.withArgs('test-state').and.returnValue('test-verifier'); // 显式调用目标方法 component.listenToRouteParameters(); // 验证调用情况 expect(cookieServiceSpy.getCookieValue).toHaveBeenCalledWith('test-state'); expect(cookieServiceSpy.removeCookie).toHaveBeenCalled(); expect(component.initiateTokenExchange).toHaveBeenCalledWith('test-code', 'test-verifier'); expect(component.refreshTokens).not.toHaveBeenCalled(); }); // 测试场景2:存在state和code,但无对应codeVerifier it('should refresh tokens when state and code exist but codeVerifier is missing', () => { queryParamMapSpy.get.withArgs('state').and.returnValue('test-state'); queryParamMapSpy.get.withArgs('code').and.returnValue('test-code'); cookieServiceSpy.getCookieValue.withArgs('test-state').and.returnValue(undefined); component.listenToRouteParameters(); expect(cookieServiceSpy.getCookieValue).toHaveBeenCalledWith('test-state'); expect(cookieServiceSpy.removeCookie).not.toHaveBeenCalled(); expect(component.initiateTokenExchange).not.toHaveBeenCalled(); expect(component.refreshTokens).toHaveBeenCalled(); }); // 测试场景3:缺少state参数 it('should refresh tokens when state param is missing', () => { queryParamMapSpy.get.withArgs('state').and.returnValue(null); queryParamMapSpy.get.withArgs('code').and.returnValue('test-code'); component.listenToRouteParameters(); expect(cookieServiceSpy.getCookieValue).not.toHaveBeenCalled(); expect(component.refreshTokens).toHaveBeenCalled(); }); // 测试场景4:缺少code参数 it('should refresh tokens when code param is missing', () => { queryParamMapSpy.get.withArgs('state').and.returnValue('test-state'); queryParamMapSpy.get.withArgs('code').and.returnValue(null); component.listenToRouteParameters(); expect(cookieServiceSpy.getCookieValue).not.toHaveBeenCalled(); expect(component.refreshTokens).toHaveBeenCalled(); }); // 测试场景5:两个参数都缺失 it('should refresh tokens when both state and code are missing', () => { queryParamMapSpy.get.withArgs('state').and.returnValue(null); queryParamMapSpy.get.withArgs('code').and.returnValue(null); component.listenToRouteParameters(); expect(cookieServiceSpy.getCookieValue).not.toHaveBeenCalled(); expect(component.refreshTokens).toHaveBeenCalled(); });
CookieService代码
@Injectable({ providedIn: 'root' }) export class CookieService { constructor() { } /** * Set cookie * @param state State value * @param codeVerifier Code verifier value */ setCookie(state: string, codeVerifier: string): void { document.cookie = `app.txs.${state}=${codeVerifier};secure;sameSite=strict;`; } /** * Get cookie value * @param state * @returns */ getCookieValue(state: string | null): string | undefined { return document.cookie.split('; ').find(row => row.startsWith(`app.txs.${state}=`))?.split('=')[1]; } /** * Remove cookie */ removeCookie(): void { let cookies = document.cookie.split(";"); for (let i = 0; i < cookies.length; i++){ let spcook = cookies[i].split("="); document.cookie = spcook[0] + "=;expires=Thu, 21 Sep 1979 00:00:01 UTC;"; } } }
内容的提问来源于stack exchange,提问作者ONE_FE
相关产品推荐
相关产品推荐

