How to test a routing resolver in Angular application
If you did not read "How to test a route", please do it first.
When we want to test a resolver it means we need to mock everything except the resolver and RouterModule
.
Optionally, we can disable guards to avoid influence of their mocked methods returning falsy values and blocking routes.
beforeEach(() =>
MockBuilder(
// Things to keep and export.
[
DataResolver,
RouterModule,
RouterTestingModule.withRoutes([]),
NG_MOCKS_ROOT_PROVIDERS,
],
// Things to mock.
TargetModule,
).exclude(NG_MOCKS_GUARDS)
);
To test the resolver we need to render RouterOutlet
.
const fixture = MockRender(RouterOutlet, {}); // {} is required to leave inputs untouched.
Additionally, we also need to properly customize mocked services if the resolver is using them to fetch data.
const dataService = TestBed.get(DataService);
dataService.data = () => from([false]);
The next step is to go to the route where the resolver is, and to trigger initialization of the router.
const location = TestBed.get(Location);
const router = TestBed.get(Router);
location.go('/target');
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick();
}
Because data is provided to a particular route, we need to find its component in the fixture
and
to extract ActivatedRoute
from its injector.
Let's pretend that /target
renders TargetComponent
.
const el = ngMocks.find(fixture, TargetComponent);
const route: ActivatedRoute = el.injector.get(ActivatedRoute);
Profit, now we can assert the data the resolver has provided.
expect(route.snapshot.data).toEqual({
data: {
flag: false,
},
});
Live example
import { Location } from '@angular/common';
import { Component, Injectable, NgModule } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import {
ActivatedRoute,
Resolve,
Router,
RouterModule,
RouterOutlet,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { combineLatest, from, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import {
MockBuilder,
MockRender,
NG_MOCKS_ROOT_PROVIDERS,
ngMocks,
} from 'ng-mocks';
// A simple service simulating a data request.
@Injectable()
class DataService {
protected flag = true;
public data(): Observable<boolean> {
return from([this.flag]);
}
}
// A resolver we want to test.
@Injectable()
class DataResolver implements Resolve<{ flag: boolean }> {
public constructor(protected service: DataService) {}
public resolve() {
return combineLatest([this.service.data()]).pipe(
map(([flag]) => ({ flag })),
);
}
}
// A resolver we want to ignore.
@Injectable()
class MockResolver implements Resolve<{ mock: boolean }> {
protected mock = true;
public resolve() {
return of({ mock: this.mock });
}
}
// A dummy component.
// It will be replaced with a mock copy.
@Component({
selector: 'target',
template: 'target',
})
class TargetComponent {}
// Definition of the routing module.
@NgModule({
declarations: [TargetComponent],
exports: [RouterModule],
imports: [
RouterModule.forRoot([
{
component: TargetComponent,
path: 'target',
resolve: {
data: DataResolver,
mock: MockResolver,
},
},
]),
],
providers: [DataService, DataResolver, MockResolver],
})
class TargetModule {}
describe('TestRoutingResolver', () => {
// Because we want to test the resolver, it means that we want to
// test its integration with RouterModule. Therefore, we pass
// the resolver as the first parameter of MockBuilder. Then, to
// correctly satisfy its initialization, we need to pass its module
// as the second parameter. And, the last but not the least, we
// need to keep RouterModule to have its routes, and to
// add RouterTestingModule.withRoutes([]), yes yes, with empty
// routes to have tools for testing.
beforeEach(() => {
return MockBuilder(
[
DataResolver,
RouterModule,
RouterTestingModule.withRoutes([]),
NG_MOCKS_ROOT_PROVIDERS,
],
TargetModule,
);
});
// It is important to run routing tests in fakeAsync.
it('provides data to on the route', fakeAsync(() => {
const fixture = MockRender(RouterOutlet, {});
const router: Router = fixture.point.injector.get(Router);
const location: Location = fixture.point.injector.get(Location);
const dataService: DataService =
fixture.point.injector.get(DataService);
// DataService has been replaced with a mock copy,
// let's set a custom value we will assert later on.
dataService.data = () => from([false]);
// Let's switch to the route with the resolver.
location.go('/target');
// Now we can initialize navigation.
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick(); // is needed for rendering of the current route.
}
// Checking that we are on the right page.
expect(location.path()).toEqual('/target');
// Let's extract ActivatedRoute of the current component.
const el = ngMocks.find(TargetComponent);
const route: ActivatedRoute = el.injector.get(ActivatedRoute);
// Now we can assert that it has expected data.
expect(route.snapshot.data).toEqual(
jasmine.objectContaining({
data: {
flag: false,
},
}),
);
}));
});