Home > OS >  Can't test simple Angular 12 guard with Karma
Can't test simple Angular 12 guard with Karma

Time:11-27

I have tried several times, but it seems like I cannot create a unit test for a very basic Guard in Angular 12 which has

  • canActivate
  • canActivateChild

as its main methods. Please find the following code:

@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);
  }
}

The authService call inside the canActivate method shall return an Observable obtained by a BehaviourSubject object using the asObservable() call. I have tried every possible test, but it seems like no comparison performed (toBe,toEqual, etc.) works for those two methods, nor is the spy on the navigation is triggered when the redirect is performed.

The following is a sample spec.ts class I created following some guides on the web:

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));
        });
      });
    });
  });
});

When I run the test file, I get something like this:

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 ...

Apparently, Karma expects a ScalarObservable of some sort, plus the navigation towards ['/login'] is not detected.

Would you mind giving me some advice on how to perform this test?

Thank you in advance.

CodePudding user response:

Here is how I would configure TestBed module and test guard:

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();
      });
    });
  });
});

CodePudding user response:

Thank you, @vitaliy.

I adjusted some things in both the guard itself and the test file, and managed to get it through.

Here is the final test file:

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']);
      });
    });
  });
});

CodePudding user response:

You do not need the mockRouter as you can add the RouterTestingModule in the imports array and do a

  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
  });

As you are subscribing in a test you need to add the waitForAsync() because the Test should wait until every observable has been executed and finished.

For example

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

Otherwise it may be that the test has been finished before the subscribe has been called, and you expect nothing.

  • Related