Testing route parameters 

You will have some component that does routing and some components that are being routed to. Sometimes components that are being routed to have a parameter, and typically their route looks something like this: /jedis/:id. The component then has the mission of digging out the ID parameter and doing a lookup on the specific Jedi that matches this ID. So, a call to a service will be made and the response should populate  a suitable parameter in our component that we then can show in the template. Such a component will typically look like this, in its entirety:

import { ActivatedRoute, Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';

import { Jedi } from './jedi.model';
import { JediService } from './jedi.service';

@Component({
selector: 'detail-component',
templateUrl: 'detail.component.html'
})
export class ExampleRoutingParamsComponent{
jedi: Jedi;
constructor(
private router: Router,
private route: ActivatedRoute,
private jediService : JediService
) {
route.paramMap.subscribe( p => {
const id = p.get('id');
jediService.getJedi( id ).subscribe( data => this.jedi = data );
});
}
}

Worth highlighting is how we get hold of the parameter in the router. We interact with the ActivatedRouter instance, that we named as route and its paramMap property, which is an observable, like so:

route.paramMap.subscribe( p => {
const id = p.get('id');
jediService.getJedi(id).subscribe( data => this.jedi = data )
})

So what do we want to test for? We would like to know that if a certain route contains an ID parameter, then our jedi property should be properly populated, through our service. We don't want to do an actual HTTP call, so our JediService will need to be mocked somehow and there is another thing complicating it, namely that route.paramMap will need to be mocked as well and that thing is an observable.

This means we need a way to create a stub of an observable. This might sound a bit daunting but it really isn't; thanks to a Subject, we can make this quite easy for ourselves. A subject has the nice ability of being something we can subscribe to, but we can also pass it values. With that knowledge, let's start to create our ActivatedRouteStub:

import { convertToParamMap } from '@angular/router';

class ActivatedRouteStub {
private subject: Subject<any>;

constructor() {
this.subject = new Subject();
}

sendParameters( params : {}) {
this.subject.next(convertToParamMap(params)); // emitting data
}

get paramMap() {
return this.subject.asObservable();
}
}

Now, let's explain this code, we add the sendValue() method so it can pass the value we give it to the subject. We expose the paramMap property, as an observable, so we can listen to the subject when it emits any values. How does this correlate to our test though? Well, calling sendValue on the stub is something we want to do in the setup phase, that is inside of a beforeEach(). This is a way for us to simulate reaching our component through routing while passing a parameter. In the test itself, we want to listen for when a router parameter is being sent to us so we can pass it on to our jediService. So, let's start sketching on the test. We will build the test in two steps:

  1. The first step is to support the mocking of the ActivatedRoute by passing the  ActivatedRouteStub.
  2. The second step is to set up the mocking of the jediService, ensuring that all HTTP calls are intercepted, and that we are able to respond with mock data when an HTTP call occurs.

For the first step, we set up the test as we have done so far by calling TestBed.configureTestingModule() and passing it an object. We mentioned that we built a stub for an activated route already and we need to make sure that we provide this instead of the real ActivatedRoute. This looks like the following code:

describe('A detail component', () => {
let fixture, component, activatedRoute;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [{
provide: ActivatedRoute,
useClass: ActivatedRouteStub
},
JediService
]
})
})
})

This means that when our component gets the ActivatedRoute dependency injected in its constructor, it will instead inject ActivatedRouteStub, like so:

@Component({})
export class ExampleRoutingParamsComponent {
// will inject ActivatedRouteStub
constructor(activatedRoute: ActivatedRoute) {}
}

Moving on with our test, we need to do three things:

  • Instantiate the component
  • Feed a route parameter to our ActivatedRouteStub so that a routing parameter is emitted
  • Subscribe to the ActivatedRouteStub so we can assert that a parameter is indeed emitted

Let's add these to our test code:

beforeEach(() => {
fixture = TestBed.createComponent(ExampleRoutingParamsComponent);
component = fixture.debugElement.componentInstance;
activatedRoute = TestBed.get(ActivatedRoute);
})

Now we have set up the fixture, the component, and our activatedRouteStub. The next step is to feed the activatedRouteStub the actual routing parameter, and to set up a subscribe of the activatedRouteStub so we know when we receive a new routing parameter. We do this inside the test itself, instead of the beforeEach() method, like so:

it('should execute the ExampleRoutingParamsComponent', () => {
// listen for the router parameter
activatedRoute.paramMap.subscribe(para => {
const id = para.get('id');
// assert that the correct routing parameter is being emitted
expect(id).toBe(1);
});
// send the route parameter
activatedRoute.sendParameters({ id : 1 });
})

So what does this mean for our component? How much of our component have we tested at this stage? Let's have a look at our DetailComponent and highlight the code covered by our test so far:

@Component({})
export class ExampleRoutingParamsComponent {
constructor( activatedRoute: ActivatedRoute ) {
activatedRoute.paramMap.subscribe( paramMap => {
const id = paramMap.get('id');
// TODO call service with id parameter
})
}
}

As you can see, we have, in the test, covered the mocking of the activatedRoute and managed to subscribe to it. What is missing on both the component and test is to account for there being a call to a service that in turn calls HTTP. Let's start with adding that code to the component, like so:

@Component({})
export class ExampleRoutingParamsComponent implements OnInit {
jedi: Jedi;
constructor(
private activatedRoute: ActivatedRoute,
private jediService: JediService ) {}

ngOnInit() {
this.activatedRoute.paramMap.subscribe(route => {
const id = route.get('id')
this.jediService.getJedi(id).subscribe(data => this.jedi = data);
});
}
}

In the code, we added the Jedi field as well as a call to this.jediService.getJedi(). We subscribed to the result and assigned the result of the operation to the Jedi field. Adding testing support for this part is something we have already covered in the previous section on mocking the HTTP. It's good to repeat this, so let's add the necessary code to the unit test, like so:

it('should call the Http service with link /api/jedis/1', () => {
.. rest of the test remains the same

const jediService = TestBed.get(JediService);
const http = TestBed.get(HttpTestingController);

// fake response
const expected = { name: 'Luke', id: 1 };
let actual = {};
http.expectOne('/api/jedis/1').flush(expected);

... rest of the test remains the same
})

What we did here is to get a copy of our JediService by asking for it from the TestBed.get() method. Furthermore, we asked for an instance of the   HttpTestingController. We move on by defining the expected data that we want to respond with, and we instruct the instance of the HttpTestingController that it should expect a call to /api/jedis/1, and when that happens then the expected data should be returned. So now we have a test that covers both the scenario of testing for the ActivatedRoute parameters, as well as the HTTP call. The full code of the test looks like the following:

import { Subject } from 'rxjs/Rx';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { TestBed } from '@angular/core/testing';
import {
HttpClientTestingModule,
HttpTestingController

}
from "@angular/common/http/testing";

import { JediService } from './jedi-service';
import { ExampleRoutingParamsComponent } from './example.routing.params.component';

class ActivatedRouteStub {
subject: Subject<any>;
constructor() {
this.subject = new Subject();
}

sendParameters(params: {}) {
const
paramMap = convertToParamMap(params);
this.subject.next( paramMap );
}

get paramMap() {
return this.subject.asObservable();
}
}

describe('A detail component', () => {
let activatedRoute, fixture, component;

beforeEach(async() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule ],
declarations: [ ExampleRoutingParamsComponent ],
providers: [
{
provide: ActivatedRoute, useClass: ActivatedRouteStub },
JediService
]
});
})

beforeEach(() => {
fixture = TestBed.createComponent(ExampleRoutingParamsComponent);
component = fixture.componentInstance;
activatedRoute = TestBed.get(ActivatedRoute);
});

it('should call the Http service with the route /api/jedis/1 and should display the jedi name corresponding to the id number in the route', async() => {
activatedRoute.paramMap.subscribe((para) => {
const id = para.get('id');
expect(id).toBe(1);
});

activatedRoute.sendParameters({ id : 1 });

const http = TestBed.get(HttpTestingController);
// fake response
const expected = { name: 'Luke', id: 1 };
let actual = {};

http.expectOne('/api/jedis/1').flush(expected);
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(component.jedi.name).toBe('Luke');
});
});
});

So what have we learned from testing route parameters? It is a bit more cumbersome as we need to create our ActivatedRouteStub, but all in all, it is quite straightforward.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset