First of all, I don't know what happens inside mockLogin()
. For the purpose of this answer, I assume it returns some dummy data and then it is being compared in Guard.
In this example loginInfo()
method returns just a simple string:
@Injectable({
providedIn: 'root'
})
export class SomeService {
loginInfo(): Observable<string> {
return of('');
}
}
I did some adjustment to the guard itself. Please note, if the first condition is met, we just pass a true value to let the router navigate. No need for another router.navigateByUrl()
call. Since I used switchMap here instead of map, we have to return either Observable or Promise. That's why I exchanged router.parseUrl()
to router.navigateByUrl()
, which returns Promise by default.
SwitchMap operator automatically completes inner observable and prevents memory leaks.
export const testGuard: CanActivateFn = () => {
const someService = inject(SomeService);
const router = inject(Router);
return someService.loginInfo().pipe(
switchMap((res: string): Observable<boolean> | Promise<boolean> => {
if (res === 'foo') {
return of(true);
}
if (res === 'bar') {
return router.navigateByUrl('/PathB');
}
return router.navigateByUrl('/Fail');
})
);
};
Now, coming back to the test setup. I moved mockService declaration outside describe()
scope:
const mockService = {
loginInfo: jest.fn()
};
Then I declared three independent dummy components, for better test results:
@Component({ template: '' })
export class FirstDummyComponent {}
@Component({ template: '' })
export class SecondDummyComponent {}
@Component({ template: '' })
export class ThirdDummyComponent {}
Then, my describe()
function looks like below. I moved spy declaration inside it()
block and made mocking an one-liner. I created RouterTestingHarness in one step, and triggered guard later with navigateByUrl()
method. If you pass a second argument into this method, which won't be rendered it will throw an error. It works like an assertion. If you always expect one instance of components in each test you can pass it.
describe('Test Guard', () => {
let someService: SomeService;
let router: Router;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
{
provide: SomeService,
useValue: mockService
},
provideRouter([
{ path: 'PathA', component: FirstDummyComponent, canActivate: [testGuard] },
{ path: 'PathB', component: SecondDummyComponent },
{ path: 'Fail', component: ThirdDummyComponent }
])
]
});
router = TestBed.inject(Router);
someService = TestBed.inject(SomeService);
});
it('test1', async () => {
const spy = jest.spyOn(someService, 'loginInfo').mockReturnValueOnce(of('foo'));
const harness = await RouterTestingHarness.create();
const activatedComponent = await harness.navigateByUrl('/PathA', FirstDummyComponent); // we can pass the expected component as a second parameter here
expect(spy).toHaveBeenCalledTimes(1); // we ensure guard and navigation were called just once
expect(router.url).toBe('/PathA'); // we check if we are under proper url
});
it('test2', async () => {
const spy = jest.spyOn(someService, 'loginInfo').mockReturnValueOnce(of('bar'));
const harness = await RouterTestingHarness.create();
const activatedComponent = await harness.navigateByUrl('/PathA'); // it also works without second argument. We can check the component instance later manually
expect(spy).toHaveBeenCalledTimes(1);
expect(activatedComponent).toBeInstanceOf(SecondDummyComponent); // manual instance check
expect(router.url).toBe('/PathB');
});
it('test3', async () => {
const spy = jest.spyOn(someService, 'loginInfo').mockReturnValueOnce(of(''));
const harness = await RouterTestingHarness.create();
const activatedComponent = await harness.navigateByUrl('/PathA', ThirdDummyComponent); // we can also combine passing second argument here + checking activatedComponent instance type below.
expect(spy).toHaveBeenCalledTimes(1);
expect(activatedComponent).toBeInstanceOf(ThirdDummyComponent);
expect(router.url).toBe('/Fail');
});
});