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

如何为Angular中实现CanActivateFn的守卫编写单元测试?

Angular中CanActivateFn类型守卫的单元测试方案

你之前的测试代码报错,原因是AuthorizedUserClaimGuard是函数类型的守卫(CanActivateFn),不是类,没法用new实例化,也不存在canActivate方法——这类守卫直接调用函数本身即可,但要借助Angular测试环境处理inject的依赖注入。

测试核心思路

  • 用TestBed配置测试模块,提供AuthenticationService和ConfigurationService的模拟实现
  • 无需实例化守卫,直接调用守卫函数并传入模拟的route和state参数
  • 借助TestBed.runInInjectionContext执行守卫函数,确保inject能正常获取依赖

完整测试代码示例

import { TestBed } from '@angular/core/testing';
import { CanActivateFn, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthorizedUserClaimGuard } from './authorized-user-claim.guard';
import { AuthenticationService } from './authentication.service';
import { ConfigurationService } from './configuration.service';

// 模拟AuthenticationService
class MockAuthenticationService {
  authenticated = false;
  private redirectRoute = '';

  setState(state: { route: string }) {
    this.redirectRoute = state.route;
  }

  logout() {}

  hasPermission(claim: string): boolean {
    return false;
  }
}

// 模拟ConfigurationService
class MockConfigurationService {
  authorizedClaims: string[] = [];

  setConfiguration(config: { authorizedClaims: string[] }) {
    this.authorizedClaims = config.authorizedClaims;
  }
}

// 生成模拟路由快照
const createMockRoute = (): ActivatedRouteSnapshot => {
  return {} as ActivatedRouteSnapshot;
};

// 生成模拟路由状态快照
const createMockRouteState = (url = '/test'): RouterStateSnapshot => {
  return { url } as RouterStateSnapshot;
};

describe('AuthorizedUserClaimGuard', () => {
  let authService: MockAuthenticationService;
  let configService: MockConfigurationService;
  let guard: CanActivateFn;

  beforeEach(() => {
    authService = new MockAuthenticationService();
    configService = new MockConfigurationService();

    TestBed.configureTestingModule({
      providers: [
        { provide: AuthenticationService, useValue: authService },
        { provide: ConfigurationService, useValue: configService }
      ]
    });

    guard = AuthorizedUserClaimGuard;
  });

  it('未认证用户:调用logout、设置跳转路由并返回false', () => {
    const logoutSpy = spyOn(authService, 'logout');
    const setStateSpy = spyOn(authService, 'setState');
    const route = createMockRoute();
    const state = createMockRouteState('/protected');

    const result = TestBed.runInInjectionContext(() => guard(route, state));

    expect(setStateSpy).toHaveBeenCalledWith({ route: '/protected' });
    expect(logoutSpy).toHaveBeenCalled();
    expect(result).toBeFalse();
  });

  it('已认证且拥有匹配权限:返回true', () => {
    authService.authenticated = true;
    configService.authorizedClaims = ['admin', 'editor'];
    spyOn(authService, 'hasPermission').and.callFake(claim => claim === 'editor');

    const route = createMockRoute();
    const state = createMockRouteState();

    const result = TestBed.runInInjectionContext(() => guard(route, state));

    expect(authService.hasPermission).toHaveBeenCalledWith('admin');
    expect(authService.hasPermission).toHaveBeenCalledWith('editor');
    expect(result).toBeTrue();
  });

  it('已认证但无匹配权限:返回false', () => {
    authService.authenticated = true;
    configService.authorizedClaims = ['admin', 'owner'];
    spyOn(authService, 'hasPermission').and.returnValue(false);

    const route = createMockRoute();
    const state = createMockRouteState();

    const result = TestBed.runInInjectionContext(() => guard(route, state));

    expect(result).toBeFalse();
  });
});

关键修改说明

  • 移除了实例化守卫的代码,直接引用守卫函数本身
  • 用TestBed.runInInjectionContext包裹守卫调用,解决inject的依赖注入问题
  • 覆盖了守卫的所有逻辑分支:未认证、已认证有权限、已认证无权限
  • 通过spyOn监控关键方法调用,验证守卫行为符合预期

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:08:28