RouterTestingModule can be used to test routed Angular components.
A routed component is a component which is the target of a navigation in that it's part of a configured route. They could be page components, shell components, layout components, dialogs, or nested components.
Some routed Angular components rely on dynamic route parameters such as our use case today, the hero detail component from the Tour of Heroes tutorial on Angular.io.
Routed components are usually dynamically rendered by a primary router outlet, but others might have routes configured for auxiliary router outlets such as a
drawer named router outlet.
Today, we're first going to walk through well-structured example of a shallow routed component test which is isolated from services and routing.
Then we'll create an integrated routed component test using the router testing Angular module. To understand what the
RouterTestingModule does, refer to "Testing Angular routing components with the RouterTestingModule".
Our case study is routed component test suites for the
HeroDetailComponent from the Tour of Heroes tutorial on Angular.io. Figure 1 illustrated that when navigation to the dynamic
/detail/:id route is triggered, the hero detail component is rendered with the name of the relevant hero in its title.
What happens is this:
- Navigation is triggered from some routing component. Which routing component is out of scope for this test suite.
- The hero detail component needs to load the hero data based on the specified
- The hero name is displayed in the hero detail component's title element.
Now, let's get started!
Shallow routed component test
The Angular testing guide explains how to implement isolated unit tests around a routed component. Our component under test is the
HeroDetailComponent from the Tour of Heroes tutorial.
You can review the model and template of the component in Listings 1A and 1B.
The hero detail component is a routed component as it has a configured route which can be seen in Listing 2.
Let's begin by creating an isolated component test. We'll create a shallow component test for the
HeroDetailComponent. It's shallow because we intentionally don't render all or any child components.
Our component under test doesn't have any child components at this point, but the strategy and techniques used are the same as if it had child components which means that we can more easily refactor it into multiple components as it grows, without breaking this test case.
Shallow routed component test utilities
Starting out, let's walk through the test utilities in Listing 3A.
getTitle function returns the text content of the heading in the hero detail component by querying its debug element and accessing the native element.
navigateByHeroId function sets the
id route parameter on the activated route stub to simulate a route change.
I took the
ActivatedRouteStub from the official Angular testing guide, but extended it to support route snapshots with the
snapshot property getter.
These are all the test utilities we need for this test case.
Shallow routed component test setup
Next up is the test setup and variables for our routed component test. Take a look at Listing 3B.
First we create a fake
HeroService (1) to load data for a specific hero based on an ID. We replace the
ActivatedRoute service with a stub.
We declare the component under test (3) and the
FormsModule (4) to be able to render its component template. We use the
CUSTOM_ELEMENTS_SCHEMA (5) to prevent rendering of view child components.
We'll use the
fakeHeroes variable directly in the test case as it holds the same data as used for the fake hero service. The
fixture variable is a reference to the component fixture for the hero detail component. Finally, we store a reference to the
routeStub which we will use to simulate routing parameters.
Shallow routed component test case
The test case in Listing 3C exercises the behaviour of our component in the happy path scenario, that is a valid hero ID is passed as a route parameter.
First, we simulate navigation to a route with a valid hero ID (1). Then we trigger change detection (2) to let our component update the DOM. Remember that the
navigateByHeroId test utility stubs a parameter in the activated route stub.
Finally, we query for the title and expect it to contain the hero name in upper-case letters (3).
Shallow routed component test suite
The full shallow routed component test suite is shown in full length in Listing 4 for reference.
This was a simple happy path test case. We could additionally add test cases that demonstrate what happens when the specified hero ID doesn't exist, is empty or otherwise invalid. This is left as an exercise for you.
Integrated routed component test with the
To get a higher level of confidence, we'll exercise how the
HeroDetailComponent integrates with a real
ActivatedRoute service and in-memory stubs of services related to routing.
We'll use the
RouterTestingModule to set up a testing route and replace the
Location service to abstract away browser APIs as explained in "Testing Angular routing components with the RouterTestingModule".
Integrated routed component test utilities
In Listing 5A, we see the test utilities required for our routed component test of the
HeroDetailComponent from the Tour of Heroes tutorial.
We will soon see that our integrated test is simulating a tiny application with a single route for our component. The test root component has a router outlet (1) which will render our component once it's configured in a test route. The
TestRootComponent is simulating what's commonly named the
What we're exercising here is our application's behaviour when a navigation to the hero detail component happens. In this test case, we're not interested in knowing which routing component triggered the navigation.
If we wanted to implement an integration test that exercise a full use case starting at the
DashboardComponent and clicking a hero link to navigate to the hero detail, we would add a default route for the dashboard component and click a hero link by first querying for and accessing a debug element through the root component fixture.
This would be an excellent behaviour test to include. Refer to "Testing Angular routing components with the RouterTestingModule" for implementation details on how to do most of this. We could even add an end-to-end test for even more confidence in our application at runtime.
advance function flushes the
NgZone task queues and runs change detection to stabilize the testing environment. As it uses
tick, it has to be called from within a
getTitle test utility function is exactly the same as in the shallow routed component test, except it refers to a
rootFixture variable now. The component fixture in this test suite refers to the
TestRootComponent, not the component under test.
navigateByHeroId function uses the provided
Router service to navigate to the
detail/:id route path, replacing the route parameter with the specified ID. The
NgZone#run-wrapped callback fixes Angular warnings related to the Angular zone.
As Angular issue #25837 discusses, Angular outputs a warning when we trigger navigation outside of a test case – usually in
beforeEach hooks. Our callback is executed inside the Angular zone to prevent this warning even though it doesn't matter in tests.
Integrated routed component test setup
Listing 5B shows how we provide the same fake hero service (1) as in the shallow routed component test.
We configure the Angular testing module by our component under test (2), the
We also declare the fake root component (3) which is the component that our component fixture will wrap (4). The
TestRootComponent has a router outlet that will render the component under test as soon as the test route (5) targets it.
We use the
RouterTestingModule#withRoutes to set up the test route, the
Router service and a fake
Location service. (6)
Like in our shallow routed component test, we import the
FormsModule (7) to render the form directives in our component template. The
CommonModule is automatically added by the Angular testing module.
After creating the component fixture, we store a reference to the provided
Router service in the
router variable (8) which is used by the
navigateByHeroId test utility function.
Finally, we have the
fakeHeroes variable (9) similar to the shallow routed component test setup. We will use this variable to get the ID of a hero which the fake hero service can resolve.
Note that we didn't perform any initial navigation as the scope of our test case is limited to what happens when and after navigating to the hero detail. It doesn't include the routing component that triggers the navigation.
As there's no initial navigation, we don't have to call
advance in our setup to let the component fixture and the following change detection cycle settle.
The test setup is not too bad. We isolate at the data service level by providing a fake
HeroService and at the browser API level by using the
If our component under test had view child components, we might also declare them as part of this test setup to integrate more software artifacts and raise the level of confidence this test gives us.
The final place of isolation is the amount of routes involved in this test suite. As discussed, we only have a test route for the hero detail component, but we could choose to include a routing component as well.
Integrated routed component test case
After discussing every piece of the test setup, let's move on to the test case. Hopefully, our tenacious test environment allows us to have a very terse test case.
The integrated test case in Listing 5C looks very similar to our shallow routed component test case, except we have to use the
advance test utility function (1).
This is because our test is integrated with real or fake services used for routing and navigation rather than an activated route stub. We're now exercising how the hero detail component reacts to a the real
In this test suite, we also use the real
Router service to navigate to a hero detail route, but this is abstracted away in the
navigateByHeroId test utility (2).
Integrated routed component test suite
For reference, we the full integrated routed component test suite is shown in Listing 6.
Notice again that we're using the real
Router service to navigate to a
/detail/:id route (1) and that this is done within the Angular zone.
The route paths and URL segments in (2) might seem like magic strings, but in fact we could have used any test route path as long as it had a dynamic
:id route parameter.
Our test route happened to match the real one used in the Tour of Heroes application, but this is coincidental, not intentional. If we wanted to have an even higher level of integration, we need to store the hero detail route path in a place that we can access both in tests, at compile time and at runtime.
See Listings 3.1, 3.2, and 3.3 of "Lean Angular components" for a simple example of solving this issue or try out Routeshub by Max Tarsis. Routeshub is a route management library that integrates easily with the Angular router.
We have test suites exercising what we would think is very simple runtime behaviour. Even though what we're verifying seems simple, a decent amount of test setup and utilities are required to allow concise test cases.
What did we test in our routed component test suite?
We verified that the hero's name is displayed in the component title when we navigate to the hero detail route.
I briefly mentioned that the router testing Angular module abstracts navigation-related browser APIs away through fake services, but that it allows us to use the real
Router service. However, I didn't mention that the
RouterTestingModule also prevents Karma from navigating away from the Karma test page and adds support for test runners without browser APIs such as the History and Location APIs.
We can remove the need for navigating through the
Router service if we scope our integrated routed component test to include the routing component which triggers navigation to our component under test.
With the increased scope, we add the routing component to the default test route and use the
Router#initialNavigation method to set our starting point.
RouterTestingModule not only allows us to integrated our tests with the real
Router service. More importantly for the test case used as a case study in this article, it enables our tests to exercise our component interacting with the real
An interesting point about the
HeroDetailComponent is that it's both a routed component and a routing component. We've seen that it's the target of a dynamic route, but it also uses the
Location service to navigate back to the
DashboardComponent in the Tour of Heroes application which makes it a routing component.
If you'd like to test this behaviour as well, follow the techniques demonstrated in "Testing Angular routing components with the RouterTestingModule".
Finally, I would like to extend a big thank you to you! Your interest in my previous article, "Testing Angular routing components with the RouterTestingModule", ignited my interest in exploring the