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

如何解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 15:24:10