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

如何正确编写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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 01:45:34