Component Testing: Routing Components (DRAFT) 4.0


If you notice any issues with this page, please report them.

This page describes how to test routing components using real or mock routers. Whether or not you mock the router will, among other reasons, depend on the following:

  • The degree to which you wish test your component in isolation
  • The effort you are willing to invest in coding mock router behavior for your particular tests

Running example

This page uses the heroes app from part 5 of the tutorial as a running example. The app component of that revision of the app is just a shell delegating functionality to either the dashboard or heroes components.

The sample tests for each of these components have been written in a complementary manner: the heroes component tests use a mock router, whereas those for the dashboard use a real router.

In test code excerpt given below, you can see the declaration of a MockRouter class, and a mockRouter instance. As described in the section on Component-external services: mock or real you must add the mock router to the providers list of the NgTestBed:

toh-5/test/heroes.dart (excerpt)

NgTestFixture<HeroesComponent> fixture; HeroesPO po; final mockRouter = new MockRouter(); class MockRouter extends Mock implements Router {} @AngularEntrypoint() void main() { final testBed = new NgTestBed<HeroesComponent>().addProviders([ provide(Router, useValue: mockRouter), HeroService, ]); setUp(() async { fixture = await testBed.create(); po = await fixture.resolvePageObject(HeroesPO); }); tearDown(disposeAnyRunningTest); // ··· }

Selecting a hero from the heroes list causes a “mini detail” view to appear:

Mini Hero Detail

Clicking the “View Details” button should cause a request to navigate to the corresponding hero’s detail view. The button’s click event is bound to the gotoDetail() method which is defined as follows:

toh-5/lib/src/heroes_component.dart (gotoDetail)

Future<Null> gotoDetail() => _router.navigate([ 'HeroDetail', {'id': selectedHero.id.toString()} ]);

You could test for this behavior by ensuring that the mock router receives a Router.navigate() request with the appropriate link parameter list.

In the following test excerpt:

  • The setUp() method selects a hero.
  • The test expects a single call to the mock router’s navigate() method, with the “HeroDetail” route as destination and the target hero’s id as a parameter.

void selectedHeroTests() { const targetHero = const {'id': 15, 'name': 'Magneta'};

setUp(() async { await po.selectHero(4); po = await fixture.resolvePageObject(HeroesPO); });

// ··· test('go to detail', () async { await po.gotoDetail(); final c = verify(mockRouter.navigate(captureAny)); final linkParams = [ 'HeroDetail', {'id': '${targetHero['id']}'} ]; expect(c.captured.single, linkParams); }); // ··· } </code-pane> import 'dart:async';

import 'package:pageloader/objects.dart'; import 'utils.dart';

class HeroesPO { @FirstByCss('h2') PageLoaderElement _title;

@ByTagName('li') List<PageLoaderElement> _heroes;

@ByTagName('li') @WithClass('selected') @optional PageLoaderElement _selectedHero;

@FirstByCss('div h2') @optional PageLoaderElement _miniDetailHeading;

@ByTagName('button') @optional PageLoaderElement _gotoDetail;

Future<String> get title => _title.visibleText;

Iterable<Future<Map>> get heroes => _heroes.map((el) async => _heroDataFromLi(await el.visibleText));

Future selectHero(int index) => _heroes[index].click();

Future<Map> get selectedHero async => _selectedHero == null ? null : _heroDataFromLi(await _selectedHero.visibleText);

Future<String> get myHeroNameInUppercase async { if (_miniDetailHeading == null) return null; final text = await _miniDetailHeading.visibleText; final matches = new RegExp((r'^(.) is my hero\s$')).firstMatch(text); return matches[1]; }

Future<Null> gotoDetail() => _gotoDetail.click();

Map<String, dynamic> _heroDataFromLi(String liText) { final matches = new RegExp((r'^(\d+) (.*)$')).firstMatch(liText); return heroData(matches[1], matches[2]); } } </code-pane> </code-tabs>

Tests written using a mock router, embody significant detail concerning the router’s API, and expected argument values such as link parameter lists.

How might you write the same test using a real router? That’s covered next.

The app dashboard, from part 5 of the tutorial, supports direct navigation to hero details using router links:

toh-5/lib/src/dashboard_component.html (excerpt)

<a *ngFor="let hero of heroes" [routerLink]="['HeroDetail', {id: hero.id.toString()}]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a>

One way to test using a real router is to mock the low-level PlatformLocation class. In addition to providing a mock platform location object, the real router expects the injector to supply values for:

toh-5/test/dashboard.dart (excerpt)

NgTestFixture<DashboardComponent> fixture; DashboardPO po; final mockPlatformLocation = new MockPlatformLocation(); class MockPlatformLocation extends Mock implements PlatformLocation {} @AngularEntrypoint() void main() { final providers = new List.from(ROUTER_PROVIDERS) ..addAll([ provide(APP_BASE_HREF, useValue: '/'), provide(PlatformLocation, useValue: mockPlatformLocation), provide(ROUTER_PRIMARY_COMPONENT, useValue: AppComponent), HeroService, ]); final testBed = new NgTestBed<DashboardComponent>().addProviders(providers); // ··· }

The router queries the platform location for location properties such as path and search parameters. Initialize these appropriately for your app. In the case of the heroes app, these are initialized to the empty string using Mockito.when().thenReturn() calls.

toh-5/test/dashboard.dart (setUpAll)

setUpAll(() async { when(mockPlatformLocation.pathname).thenReturn(''); when(mockPlatformLocation.search).thenReturn(''); when(mockPlatformLocation.hash).thenReturn(''); when(mockPlatformLocation.getBaseHrefFromDOM()).thenReturn(''); });

Using this setup, the go-to-detail test illustrated in the previous section, could be written for the dashboard component as follows:

toh-5/test/dashboard.dart (go to detail)

test('select hero and navigate to detail', () async { clearInteractions(mockPlatformLocation); await po.selectHero(3); final c = verify(mockPlatformLocation.pushState(any, any, captureAny)); expect(c.captured.single, '/detail/15'); });

Contrast this with the heroes “go to details” test shown earlier. While the dashboard test requires lower-level configuration of a mock location, the test has the advantage of ensuring that route configurations are declared as expected.

Which routing components can I test?

You can test any routing component, except the app root (usually AppComponent). This is because there is a circular dependency between the router associated with the app root, and the app root instance itself.

In the context of a real app, the cyclic dependency is resolved by bootstrapping, but such a capability isn’t provided by angular_test, nor is it generally needed in the context of component testing. When the app root is a routing component, it is often only an app shell, delegating most of the work to other components. In such a case, it is these other components which will be the most useful test subjects.