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.