无法用 Karma 测试简单的 Angular 12 守卫

Can't test simple Angular 12 guard with Karma

我已经尝试了几次,但似乎我无法为 Angular 12 中的非常基本的 Guard 创建单元测试,其中有

作为其主要方法。 请找到以下代码:

@Injectable({
  providedIn: 'root'
})
export class IsAuthenticatedGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.authService.getIsAuthenticated().pipe(
      tap(isAuth => {
        if (!isAuth) {
          // Redirect to login
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
          this.router.navigate(['/login']);
        }
      })
    );
  }

  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return this.canActivate(route, state);
  }
}

canActivate 方法中的 authService 调用应 return 一个由 BehaviourSubject 对象使用 asObservable() 调用获得的 Observable。 我已经尝试了所有可能的测试,但似乎没有执行比较(toBetoEqual 等)适用于这两种方法,执行重定向时也不会触发导航间谍.

以下是示例 spec.ts class 我根据网络上的一些指南创建的:

function mockRouterState(url: string): RouterStateSnapshot {
  return {
    url
  } as RouterStateSnapshot;
}

describe('IsAuthenticatedGuard', () => {
  let guard: IsAuthenticatedGuard;
  let authServiceStub: AuthService;
  let routerSpy: jasmine.SpyObj<Router>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [SharedModule, RouterTestingModule]
    });
    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 a guarded route configuration', () => {
        it('grants route access', () => {
          const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
          expect(canActivate).toEqual(of(true));
        });
        it('grants child route access', () => {
          const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
          expect(canActivateChild).toEqual(of(true));
        });
      });
    });
  });

  describe('when the user is logged out', () => {
    beforeEach(() => {
      authServiceStub.setIsAuthenticated(false);
    });
    mockUrls.forEach(mockUrl => {
      describe('and navigates to a guarded route configuration', () => {
        it('does not grant route access', () => {
          const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
          expect(canActivate).toEqual(of(false));
        });
        it('does not grant child route access', () => {
          const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
          expect(canActivateChild).toEqual(of(false));
        });
        it('navigates to the login page', () => {
          // eslint-disable-next-line @typescript-eslint/unbound-method
          expect(routerSpy.navigate).toHaveBeenCalledWith(['/login'], jasmine.any(Object));
        });
      });
    });
  });
});

当我 运行 测试文件时,我得到这样的东西:

Expected object to have properties _subscribe: Function Expected object not to have properties source: Observable({ _isScalar: false, source: BehaviorSubject({ _isScalar: false, observers: [ ], closed: false, isStopped: false, hasError: false, thrownE rror: null, _value: false }) }) operator: MapOperator({ project: Function, thisArg: undefined }) Error: Expected object to have properties _subscribe: Function ...

显然,Karma 需要某种 ScalarObservable,而且未检测到朝向 ['/login'] 的导航。

你介意给我一些关于如何进行这个测试的建议吗?

提前谢谢你。

下面是我将如何配置 TestBed 模块和测试守卫:

describe('IsAuthenticatedGuard', () => {
  const mockRouter = {
    navigate: jasmine.createSpy('navigate'),
  };
  const authService = jasmine.createSpyObj('AuthService', ['getIsAuthenticated']);
  let guard: IsAuthenticatedGuard;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        providers: [
          IsAuthenticatedGuard,
          { provide: Router, useValue: mockRouter },
          { provide: AuthService, useValue: authService },
        ],
      }).compileComponents();
    }),
  );

  beforeEach(() => {
    guard = TestBed.inject(IsAuthenticatedGuard);
  });

  describe('when the user is logged in', () => {
    beforeEach(() => {
      authService.setIsAuthenticated.and.returnValue(of(true));
    });

    it('grants route access', () => {
      guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
        expect(result).toBeTrue();
      });
    });

    it('grants child route access', () => {
      guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
        expect(result).toBeTrue();
      });
    });
  });
});

谢谢@vitaliy。

我在守卫本身和测试文件中调整了一些东西,并设法通过了。

这是最终的测试文件:

describe('IsAuthenticatedGuard', () => {
  const mockRouter = {
    navigate: jasmine.createSpy('navigate')
  };
  const authService = jasmine.createSpyObj<AuthService>('AuthService', ['getIsAuthenticated']);
  let guard: IsAuthenticatedGuard;

  beforeEach(
    waitForAsync(() => {
      void TestBed.configureTestingModule({
        providers: [
          IsAuthenticatedGuard,
          {
            provide: Router,
            useValue: mockRouter
          },
          {
            provide: AuthService,
            useValue: authService
          }
        ]
      }).compileComponents();
    })
  );

  beforeEach(() => {
    guard = TestBed.inject(IsAuthenticatedGuard);
  });

  describe('when the user is logged in', () => {
    beforeEach(() => {
      authService.getIsAuthenticated.and.returnValue(of(true));
    });

    it('grants route access', () => {
      void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
        expect(result).toBeTrue();
      });
    });

    it('grants child route access', () => {
      guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
        expect(result).toBeTrue();
      });
    });
  });

  describe('when the user is logged out', () => {
    beforeEach(() => {
      authService.getIsAuthenticated.and.returnValue(of(false));
    });

    it('does not grant route access', () => {
      void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
        expect(result).toBeFalse();
        expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
      });
    });

    it('does not grant child route access', () => {
      guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
        expect(result).toBeFalse();
        expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
      });
    });
  });
});

您不需要 mockRouter,因为您可以在 imports 数组中添加 RouterTestingModule 并执行

  const router: Router;

  beforeEach(
    waitForAsync(() => {
      void TestBed.configureTestingModule({
        imports: [RouterTestingModule], //ADD THIS HERE
        providers: [
          IsAuthenticatedGuard,
          {
            provide: AuthService,
            useValue: authService
          }
        ]
      }).compileComponents();
    })
  );

  beforeEach(() => {
    guard = TestBed.inject(IsAuthenticatedGuard);
    router = TestBed.inject(Router); //ADD THIS HERE
  });

当您订阅测试时,您需要添加 waitForAsync() 因为测试应该等到每个可观察对象都已执行并完成。

例如

it('does not grant child route access', waitForAsync(() => {
      guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
        expect(result).toBeFalse();
        expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
      });
    }));

否则可能是调用订阅之前测试已经结束,而你expect什么都没有。