如何解决Angular 12含canActivate的路由Guard无法用Karma测试的问题
核心错误原因
- 直接比对不同Observable实例的结构:
guard.canActivate返回的是经过tap运算符包装后的Observable,和测试中用来比对的of(true)/of(false)是完全独立的实例,内部结构不匹配,所以会触发你看到的属性比对报错。 - 未订阅返回的Observable:RxJS的管道逻辑只有在订阅后才会执行,你没有订阅
canActivate/canActivateChild的返回值,导致tap里的路由跳转逻辑从未运行,路由spy自然无法被触发。
修正后的完整测试代码
import { firstValueFrom } from 'rxjs'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { IsAuthenticatedGuard } from './is-authenticated.guard'; import { AuthService } from './auth.service'; // 替换为你项目中AuthService的实际路径 function mockRouterState(url: string): RouterStateSnapshot { return { url } as RouterStateSnapshot; } describe('IsAuthenticatedGuard', () => { let guard: IsAuthenticatedGuard; let authServiceStub: AuthService; let routerSpy: jasmine.SpyObj<Router>; beforeEach(() => { authServiceStub = new AuthService(); routerSpy = jasmine.createSpyObj<Router>('Router', ['navigate']); guard = new IsAuthenticatedGuard(authServiceStub, routerSpy); }); it('should be created', () => { expect(guard).toBeTruthy(); }); const dummyRoute = {} as ActivatedRouteSnapshot; const mockUrls = ['/', '/dtm', '/drt', '/reporting']; describe('when the user is logged in', () => { beforeEach(() => { authServiceStub.setIsAuthenticated(true); }); mockUrls.forEach(mockUrl => { describe(`and navigates to ${mockUrl}`, () => { it('grants route access', async () => { const canActivate$ = guard.canActivate(dummyRoute, mockRouterState(mockUrl)); const result = await firstValueFrom(canActivate$); expect(result).toBeTrue(); }); it('grants child route access', async () => { const canActivateChild$ = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl)); const result = await firstValueFrom(canActivateChild$); expect(result).toBeTrue(); }); }); }); }); describe('when the user is logged out', () => { beforeEach(() => { authServiceStub.setIsAuthenticated(false); routerSpy.navigate.calls.reset(); // 每次用例前清空spy调用记录,避免互相干扰 }); mockUrls.forEach(mockUrl => { describe(`and navigates to ${mockUrl}`, () => { it('does not grant route access', async () => { const canActivate$ = guard.canActivate(dummyRoute, mockRouterState(mockUrl)); const result = await firstValueFrom(canActivate$); expect(result).toBeFalse(); }); it('does not grant child route access', async () => { const canActivateChild$ = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl)); const result = await firstValueFrom(canActivateChild$); expect(result).toBeFalse(); }); it('navigates to the login page', async () => { // 订阅Observable触发管道内的tap逻辑 const canActivate$ = guard.canActivate(dummyRoute, mockRouterState(mockUrl)); await firstValueFrom(canActivate$); expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']); }); }); }); }); });
关键改动说明
- 用
firstValueFrom获取Observable实际发射的值做比对,不再直接比对Observable实例本身 - 所有涉及Observable逻辑的用例都加了
async/await处理异步时序 - 测试路由跳转前先订阅
canActivate返回的Observable,触发tap内的跳转逻辑 - 新增了spy调用记录的重置逻辑,避免不同用例之间的调用记录互相干扰
内容的提问来源于stack exchange,提问作者AlexKibo88
相关产品推荐
相关产品推荐

