As I mentioned in a few places, there are two ways test router-related code. I call them "unit testing" and "integration testing". But because these two terms are used in so many contexts, read comments here to see what I mean:
Essentially, the latter one calls 'router.navigate'. In my experience, Googlers mostly care about integration testing.
Router does a very poor job at providing support for isolated using testing. Mainly, cause it is super difficult to create ActivatedRoute and RouterState data structures. If we provide some helpers to make their creation easier, we can simplify everything very nicely.
Simple case:
createActivatedRoute({
params: {x: "1"},
queryParams: {y: "2"},
data: { b: 100 },
children: [
{ params: {x: "1"}, data: { b: 200 } }
]
})
Values changing over time:
createActivatedRoute(cold('a--b|',
a: {
params: {x: "1"},
queryParams: {y: "2"},
data: { b: 100 },
children: [
{ params: {x: "1"}, data: { b: 200 } }
]
},
b: {
params: {x: "1"},
queryParams: {y: "2"},
data: { b: 100 },
children: [
{ params: {x: "1"}, data: { b: 200 } }
]
}
))
PRO:
- Resolvers are handled nicely, just via data
- Very direct
CONS:
- need to manually sync URL and params (in the example above, the URL is missing!)
- everything has to be a string, maybe confusing (need to write checks)
- router.routeConfig won't work cause there is no config
Simple case:
createActivatedRoute('/one/two?y=2', {
path: 'one',
data: {b: 100},
children: [{path: 'two', data: {b: 200}]
});
Values changing over time (without resolvers):
createActivatedRoute(
cold('a--b|', {a: '/one/two?y=2', b: '/one/two?y=3'}),
{
path: 'one',
data: {b: 100},
children: [{path: 'two', data: {b: 200}]
}
);
Values changing over time (with resolvers):
createActivatedRoute(
cold('a--b|',
{
a: {url: '/one/two?y=2', resolve: {B_ONE: 100, B_TWO: 200}},
b: {url: '/one/two?y=3', resolve: {B_ONE: 2222, B_TWO: 5555}}
}
),
{
path: 'one',
resolve: {b: 'B_ONE'},
children: [{path: 'two', resolve: {b: 'B_TWO'}]
}
);
PRO:
- Closer to what production code does. Can be used by itself and with production configuration.
- Terse
- Everything is in sync: url, params, etc
CONS:
- Indirect
- Resolvers are weird (and that's because they cannot be represented in the URL).
I thought about them for some time, and I think I prefer your suggestion. I like how direct it is. I think being direct is better than being terse.
I cover why the router already supports integration testing well here:
I also put together an example showing how to remove all the boilerplate with a 10-line helper here:
I don't think we should use the helper I put together in the repo: it hides TestBed and I'm sure folks will be confused. What I suggest is even smaller, just add this helperl
// provided automically
@Component({
selector: 'container',
template: `<router-outlet></router-outlet>`
})
class Container {}
class RouterTestingModule {
static navigate(url: string, component: Type): Promise<ComponentFixture<any>> {
TestBed.configureTestBed({
declarations: [Container]
}).compileComponents();
const c = TestBed.createComponent(Container);
c.detectChanges();
return TestBed.get(Router).navigateByUrl(url).then(() => {
c.detectChanges();
return findComponent(c, component);
});
}
}
This is how we can use it:
@NgModule({
declarations: [OneComponent],
imports: [
RouterModule.forChild([
{path: 'one', component: OneComponent}
])
]
})
class MyAppModule {
}
TestBed.configureTestingModule({
imports: [
MyAppModule,
RouterTestingModule
],
}).compileComponents();
const c = await RouterTestingModule.navigate('one', OneComponent);
expect(c.nativeElement.innertHTML).toContain('one');
We can also remove the promise and the await, if we use fakeAsync.