DEV Community

Cover image for Testing Angular route guards with the RouterTestingModule

Testing Angular route guards with the RouterTestingModule

Lars Gyrup Brink Nielsen on November 16, 2020

Original cover photo by Liam Tucker on Unsplash. Original publication date: 2020-09-19. Route guards can prevent activating or deactivating speci...
Collapse
 
faenor profile image
faenor

Very nice! I appreciate this article. I could not find any other valuable information on how to test guards nicely.

Collapse
 
omeryousaf profile image
Omer Yousaf • Edited

Too long write up but it does show the way towards navigation guard testing.
One question: why wrap navigation step inside NgZone.run when the navigation API's i-e router.navigate() and router.navigateByUrl() already return a promise ? Why not just use await like

let navigationSuccessful: boolean = await router.navigate(['/xyz']);
expect(navigationSuccessful).toBe(true);
Enter fullscreen mode Exit fullscreen mode

or

let navigationSuccessful: boolean = await router.navigateByUrl(['/xyz']);
expect(navigationSuccessful).toBe(true);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

When we use the TestBed we need to emulate parts of the Angular framework such as trigger change detection. The TestBed doesn't bootstrap an Angular application. No longer are all events caught by the NgZone triggering change detection. This is also the case here. If we don't wrap Router#navigate* in NgZone#run in tests, a warning is triggered.

Collapse
 
omeryousaf profile image
Omer Yousaf

Lars, in your tests you just check whether the navigation went through or was denied, right ?

expect(canNavigate).toBeFalse();
Enter fullscreen mode Exit fullscreen mode

So, was triggering change detection still necessary ?

Thread Thread
 
layzee profile image
Lars Gyrup Brink Nielsen

If we want to avoid warnings, yes. The warning will say that a scheduled event happened outside of the NgZone.

Thread Thread
 
layzee profile image
Lars Gyrup Brink Nielsen

We also want to make sure that changes are stable as they would be in an app.

Collapse
 
codeofarmz profile image
codeOfArmz

How would you test guards when they get created from a factory fn, and the service is injected using the new inject() function. Something like: canActivate: [ getGuard('accessFlag') ]. The isolated approach in that case seems to be challenging - inject() requires a context and you no longer use the constructor then.

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen

You can use an integrated test with RouterTestingModule as described in this article or you can use an isolated unit test that uses TestBed to set up test doubles.

// auth.guard.ts
import {
  ActivatedRouteSnapshot,
  CanActivateFn,
  Router,
  RouterStateSnapshot,
} from '@angular/router';

import { AuthService } from './auth.service';

function checkLogin(url: string): boolean {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn) {
    return true;
  }

  // Store the attempted URL for redirecting
  authService.redirectUrl = url;

  // Create a dummy session id
  const sessionId = 123456789;

  // Set our navigation extras object
  // that contains our global query params and fragment
  const navigationExtras: NavigationExtras = {
    queryParams: { session_id: sessionId },
    fragment: 'anchor',
  };

  // Navigate to the login page with extras
  router.navigate(['/login'], navigationExtras);

  return false;
}

// Other guards omitted for brevity

export const canActivateAuthGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => checkLogin(state.url);
Enter fullscreen mode Exit fullscreen mode
// auth.guard.spec.ts
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';

import { canActivateAuthGuard } from './auth.guard';
import { AuthService } from './auth.service';

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

describe('AuthGuard (isolated)', () => {
  beforeEach(() => {
    routerSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
    serviceStub = {};
    TestBed.configureTestingModule({
      providers: [
        {
          provide: AuthService,
          useValue: serviceStub,
        },
        {
          provide: Router,
          useValue: routerSpy,
        },
      ],
    });
  });

  const dummyRoute = {} as ActivatedRouteSnapshot;
  const fakeUrls = ['/', '/admin', '/crisis-center', '/a/deep/route'];
  let routerSpy: jasmine.SpyObj<Router>;
  let serviceStub: Partial<AuthService>;

  describe('when the user is logged in', () => {
    beforeEach(() => {
      serviceStub.isLoggedIn = true;
    });

    fakeUrls.forEach((fakeUrl) => {
      describe('and navigates to a guarded route configuration', () => {
        it('grants route access', () => {
          const canActivate = canActivateAuthGuard(dummyRoute, fakeRouterState(fakeUrl));

          expect(canActivate).toBeTrue();
        });
      });
    });
  });

  describe('when the user is logged out', () => {
    beforeEach(() => {
      serviceStub.isLoggedIn = false;
    });

    fakeUrls.forEach((fakeUrl) => {
      describe('and navigates to a guarded route configuration', () => {
        it('rejects route access', () => {
          const canActivate = canActivateAuthGuard(dummyRoute, fakeRouterState(fakeUrl));

          expect(canActivate).toBeFalse();
        });
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
codeofarmz profile image
codeOfArmz

Thanks, was also testing my guard factory whether it returns a callable function, but not sure it is needed - feels like not trusting TypeScript ;-) At any rate. thank you for this reply, I think it would be a good extension of your article anyway.

Thread Thread
 
layzee profile image
Lars Gyrup Brink Nielsen

Not 100% sure that the above example works. See netbasal.com/testing-di-functions-...

Collapse
 
yosiasz profile image
Josiah Solomon • Edited

Mind blowing, learned so much. Only one of it's kind that deals with routes/guards testing so thoroughly. thanks so much!

Collapse
 
1antares1 profile image
1antares1

Great, great article! Awesome... Thanks for sharing.