Search code examples
angularunit-testingjasminekarma-jasmine

Angular 16 route guard unit test observable createSpyObj property


Trying to familiarize with unit testing a route guard. I have a relatively straight forward guard that gets a value from a subject asObservable to either createUrlTree or return a boolean value.

In the browser debugger I am not able to step inside the map function to see any values and my tests are failing because the spy method was never called. Thank you in advance for any help or insight.

guard

  export const loginGuard: CanActivateFn = () => {
  const userService: UserService = inject(UserService);
  const router: Router = inject(Router);

  return userService.isLoggedIn$
    .pipe(
      take(1),
      map(loggedIn => {
        return loggedIn ? router.createUrlTree(['/dashboard']) : true;
      }));
};

test

  const executeGuard: CanActivateFn = (...guardParameters) =>
    TestBed.runInInjectionContext(() => loginGuard(...guardParameters));
  const mockRouter = jasmine.createSpyObj('Router', ['createUrlTree']);
  const mockUserService = jasmine.createSpyObj('UserService', [], {isLoggedIn$: of(true)});
  let activatedRoute: ActivatedRoute;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {provide: Router, useValue: mockRouter},
        {provide: UserService, useValue: mockUserService},
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: {}
          }
        }
      ]
    });
    activatedRoute = TestBed.inject(ActivatedRoute);
  });

  it('should redirect to dashboard', () => {
    executeGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
    expect(mockRouter.createUrlTree).toHaveBeenCalledWith('/dashboard');
  });

  it('should proceed to login', () => {
    mockUserService.isLoggedIn$ = of(false);
    const canActivate = executeGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
    expect(canActivate).toBeTruthy();
  });

Solution

  • You need to subscribe to the guard, since it returns an observable stream.

    I use fakeAsync and flush for waiting until the subscribe is completed, this might be optional, but it's good to have:

      it('should redirect to dashboard', fakeAsync(() => {
        executeGuard(activatedRoute.snapshot, 
            {} as RouterStateSnapshot).subscribe(); // <- changed here!
        flush();
        expect(mockRouter.createUrlTree).toHaveBeenCalledWith('/dashboard');
      }));
    
      it('should proceed to login', () => {
        mockUserService.isLoggedIn$ = of(false);
        const canActivate = executeGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
        expect(canActivate).toBeTruthy();
      });