如何为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
相关产品推荐
相关产品推荐

