How to unit-test functional Angular-Guards with Jasmine

Post image

What are Angular route guards?

If you’ve worked with Angular on any complex or commercial product, you’ve most likely came across Angular router navigation guards at some point or the other. This Angular mechanic allows you to control access to certain parts of your application. Angular differentiates four different types of route guards:

  • CanActivate
  • CanActivateChild
  • CanLoad
  • CanDeactivate

The most common use case for Angular routers is an authentication guard, which shields part of the application from unauthenticated access, e.g. requiring a user to login before accessing another user’s profile.

How did route guards work in Angular <= 14?

In Angular version 14 and below, Angular provided interfaces, e.g. CanActivate, which could be implemented by a class to become a guard and thus decide whether or not a route can be activated. Such classes implementing the CanActivate interface had to implement a canActivate method returning true or false to signal to the router if it should proceed with the requested navigation. Alternatively a UrlTree could be returned to stop the requested navigation and instead navigate to a different URL.

This could look roughly like in the following example.

Let’s imagine a very simple login service:

login.service.ts

import { Injectable } from "@angular/core";

@Injectable({
providedIn: "root",
})
export class LoginService {
private loggedInUser: string;

    public isLoggedIn() {
        return this.loggedInUser ? true : false;
    }

}

This service saves the currently logged-in user and allows to check if a user is logged in.

app.module.ts:

const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "login", component: LoginComponent },
{ path: "profile", component: UserProfileComponent, canActivate: [CanActivateRouteGuard] },
];

In the app.module.ts (or the app-routing.module.ts, depending on your setup) we define the routes. The “/profile” route, which allows displaying the user profile should only be accessible if the user is logged in, thus we protect it with our CanActivateRouteGuard.

can-activate-route-class.guard.ts:

import { CanActivate, Router } from "@angular/router";
import { LoginService } from "./login.service";
import { Injectable } from "@angular/core";

@Injectable({
providedIn: "root",
})
export class CanActivateRouteGuard implements CanActivate {
constructor(private router: Router, private loginService: LoginService) {}

    canActivate() {
        if (this.loginService.isLoggedIn()) {
            return true;
        }
        return this.router.createUrlTree(["login"]);
    }

}

The CanActivateRouteGuard class implements Angular’s CanActivate RouteGuard interface. This also means that we have to implement a canActivate method. In this case we check whether the user is logged in. If they are logged in, we return true, otherwise we’ll redirect them to the login page by returning a UrlTree pointing to the “/login” route. Also note that the LoginService is provided through Angular’s dependency injection mechanism in the class’ constructor in this example.

What has changed with Angular 15?

In version 14.2 Angular first introduced functional route guards as a new concept to protect parts of your application. With the release of the major version 15 they then finally deprecated the original concept of creating class-based route guards. While these changes provide a more flexible, modernized solution, they also introduced a new problem. The old class-based guards were extremely easy to test, as at the end of the day, you were just testing another class and a class method, returning a boolean. Thus, they could be handled exactly the same way every other class in your projects could be handled. Functional route guards on the other hand, proved to be a bit more tricky when it comes to unit-testing their functionality.

How has the implementation changed?

But let’s take a step back first. What exactly has changed?

In order to understand that, we’ll take a look at the above example with a functional route guard instead of the class-based one.

app.module.ts:

const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "login", component: LoginComponent },
{ path: "profile", component: UserProfileComponent, canActivate: [CanActivateRouteGuard] },
];

If we take a look at our route definition, we can see that nothing has changed here actually. You’re still able to define your routes, including your guards, exactly the way you already used to do it with the old concept.

can-activate-route.guard.ts:

import { CanActivateFn, Router } from "@angular/router";
import { inject } from "@angular/core";

export const canActivateRouteGuard: CanActivateFn = () => {
const loginService = inject(LoginService);
const router = inject(Router);

    if (loginService.isLoggedIn()) {
        return true;
    }
    return router.createUrlTree(["login"]);

};

As you can see in the code above, instead of setting up a class, importing canActivate from @angular/router and implementing the CanActivate interface, you now have to create a function. After importing CanActivateFn from @angular/router we have to define a function of that type. The content of this function is basically identical to the canActivate method from the initial example. However, as the logic is now function-based and not class-based, we can’t rely on Angular’s constructor dependency injection anymore. Instead we have to use the inject function from @angular/core to access Router and LoginService.

How to test functional route guards?

While converting all the guards of our current project to be functional guards, which was done in a flash, I’ve noticed that converting the corresponding unit-tests wouldn’t be quite as straightforward. A quick Google search later, I wasn’t any smarter, as of the time of writing this article, information on how to successfully test functional route guards with Jasmine in Angular is still quite sparse. The most tricky part of testing a functional route guard is to run the guard’s logic in a context where all the necessary dependencies are present. With the old route guard concept, Angular’s standard dependency injection mechanism for classes could be used to take care of that.

The snippets below show how unit-testing in the correct context could be achieved nonetheless. Imagine again a functional route guard (as shown above) that checks whether a user is logged in, returns true if the user is logged in and redirects the user to the login page otherwise.

import { TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, RouterStateSnapshot } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { canActivateRouteGuard } from "./can-activate-route.guard";
import { LoginService } from "./login.service";

describe("canActivateRouteGuard", () => {
const mockLoginService = jasmine.createSpyObj("LoginService", ["isLoggedIn"]);
const mockRouter = jasmine.createSpyObj("Router", ["createUrlTree"]);

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [RouterTestingModule],
            providers: [
                {
                    provide: Router,
                    useValue: mockRouter,
                },
                {
                    provide: LoginService,
                    useValue: mockLoginService,
                },
                {
                    provide: ActivatedRoute,
                    useValue: {
                        snapshot: {},
                    },
                },
            ],
        });
    });


    beforeEach(() => {
        mockLoginService.isLoggedIn.calls.reset();
        mockRouter.createUrlTree.calls.reset();
    });


    it("should return true if user is logged in", () => {
        const activatedRoute = TestBed.inject(ActivatedRoute);
        mockLoginService.isLoggedIn.and.returnValue(true);


        const guardResponse = TestBed.runInInjectionContext(() => {
            return canActivateRouteGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
        });


        expect(guardResponse).toBeTrue();
        expect(mockRouter.createUrlTree).not.toHaveBeenCalled();
    });


    it("should route to login page if user is not logged in", () => {
        const activatedRoute = TestBed.inject(ActivatedRoute);
        mockLoginService.isLoggedIn.and.returnValue(false);


        TestBed.runInInjectionContext(() => {
            return canActivateRouteGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
        });


        expect(mockRouter.createUrlTree).toHaveBeenCalledOnceWith(["login"]);
    });

});

At first glance this might be a bit much to take in, so let’s break it down.

We start off by creating spy objects (mockLoginService and mockRouter) for our dependencies. This allows us to control the output of the method calls in our test cases later on. Additionally we’re able to track whether or not a method has been called (and how often for that matter).

As common with Angular unit-tests, we then set up our TestBed and configure the TestingModule. Aside from importing the RouterTestingModule, we also provide our previously created mocks here. Note that we also provide an empty object to be returned whenever snapshot is requested on ActivatedRoute, which will prove handy in a minute.

In a second beforeEach block we then make sure any previously recorded method calls on our spy objects are reset to ensure that our test cases don’t influence each other and we’re always starting from a clean slate.

For our example, we’ve created two test cases. One for granting access to the user profile and one for denying it. Necessary dependencies can be provided through the TestBed’s inject method, as can be seen with ActivatedRoute. By manipulating the return values on our spy objects we can then set up the correct prerequisites for our test case. Here for example, we want to test our guard for a logged in user, thus we alter what isLoggedIn will return:

mockLoginService.isLoggedIn.and.returnValue(true);

To actually execute the guard code we want to test, we need to call our guard function in the EnvironmentInjector context of our TestBed. This can be achieved by utilizing the runInInjectionContext method of TestBed.

const guardResponse = TestBed.runInInjectionContext(() => {
return canActivateRouteGuard(activatedRoute.snapshot, {} as RouterStateSnapshot);
});

For the final call to our new guard function, we’ll additionally have to provide an ActivatedRouteSnapshot, which we’ll just substitute with the empty object for our testing purposes, as defined earlier in our providers array. Finally, the guard’s response is saved for subsequent assertions.

To assert potential redirects triggered by your functional guards, calls to the Router spy object can be monitored and asserted, as can be seen in this line:

expect(mockRouter.createUrlTree).toHaveBeenCalledOnceWith(["login"]);

Final thoughts

While conversion of legacy route guards has proven to be easily achievable, converting existing unit-tests might come with some challenges. However, once it’s clear how to set up the test environment appropriately and provide the correct context and dependencies, adapting existing unit-tests becomes way less intimidating.

You May Also Like