Search code examples
angularunit-testingangular-routerangular-router-guards

Minimal mock for unit testing angular Router and CanActivate guard


import {Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree} from '@angular/router';

@Injectable({provideIn: 'root'})
export class FooGuard implements CanActivate {
  constructor (private readonly router: Router) {}

  canActivate (next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<UrlTree> {
    const xxx = myMagic(next); // irrelevant app logic that depends on next url only

    return (async () => this.router.parseUrl(xxx));
  }
}

Trying to find an example of testing code for this piece without a page of extra boilerplate. Hopefully can get something closer to 5-6 lines of code per mock. Need:

  • Mock for Router
  • Mock for ActivatedSnapshot

Solution

  • Take a look at RouterTestingModule. It's not a six lines of code solution, but a pretty compact one. I think it's the best way to test guards and routes:

    import { Component, Injectable } from '@angular/core';
    import { TestBed } from '@angular/core/testing';
    import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from "@angular/router";
    import { RouterTestingModule } from '@angular/router/testing';
    
    @Injectable({
        providedIn: 'root'
    })
    export class FooGuard implements CanActivate {
        constructor (private readonly router: Router) {}
    
        canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
          const url = "some/new/url"; // create an url based on next snapshot
          return this.router.parseUrl(url);
        }
      }
    
    @Component({ template: '' })
    export class DummyComponent {}
    
    function setup(): {
        router: Router
    } {
        TestBed.configureTestingModule({
            imports: [
                RouterTestingModule.withRoutes([
                    { path: 'test', component: DummyComponent, canActivate: [ FooGuard ] },
                    { path: 'some/new/url', component: DummyComponent }
                ])
            ],
            declarations: [
                DummyComponent
            ]
        });
    
        return {
            router: TestBed.get(Router)
        };
    }
    
    describe(FooGuard.name, () => {
        it('should redirect to a new url', async () => {
            const { router } = setup();
            await router.navigateByUrl('/test');
            expect(router.url).toBe('/some/new/url');
        })
    });
    
    

    Actually the regular Router.forRoot() should also work in this case, but RouterTestingModule must be more suitable for testing. For example the last one provides custom Location implementation.