Angular Testing
Injections in the code is done using parameters of the constructor.
The corresponding test needs to have providers corresponding to these injections in the configureTestingModule function called on the TestBed.
For the method in code as below,
constructor(private roleService: RoleService, private router: Router) { }
the corresponding test has the below
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
providers: [
RoleGetResolver,
{provide: RoleService, useValue: roleServiceStub}
]
});
There are multiple ways to specify the providers corresponding to the injections in the constructor.
Use the providers array and each entry corresponds to a provider.
a) An object with ‘provide’ — ‘useValue’ pairs
{provide: RoleService, useValue: roleServiceStub}
Here, roleServiceStub is a stubbed object as below.
const roleServiceStub:any = {
get: function ():Observable<Role> {
return Observable.create({
name: ‘role-name’
});
},
};
and RoleService comes from the import
import { RoleService } from ‘./role.service’;
b) An object with ‘provide’ — ‘useClass’ pairs
{provide: Router, useClass: RouterStub}
where Router is a class imported as below
import { Router } from ‘@angular/router’;
and RouterStub is a class as below. Hence useClass attribute instead of useValue
class RouterStub {
navigateByUrl(url: string) { return url; }
}
c) Some injections are done using the module imports.
For code as below
constructor(private roleService: RoleService, private router: Router) { }
The corresponding providers in the test is
providers: [
RoleGetResolver,
{provide: RoleService, useValue: roleServiceStub}
]
Notice that there are no direct defined providers for Router.
This is injected through the imports section which is where the modules being imported are specified.
import { RouterTestingModule } from ‘@angular/router/testing’;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
providers: [
RoleGetResolver,
{provide: RoleService, useValue: roleServiceStub}
]
});
imports: [
RouterTestingModule
],
TestBed is a class that can be used to setup the testing module corresponding to your testing artifact (service/component/guard)
The TestBed class is available in angular/core/testing and is imported as below
import { TestBed, inject } from ‘@angular/core/testing’;
- TestBed can be used to configure your testing module
- TestBed has a method “configureTestingModule” that takes in a json object as the specification on how to configure.
- The sections of the json for configureTestingModule includes imports,providers etc
- Example configureTestingModule invocation looks as below
TestBed.configureTestingModule({
imports: [
HttpModule,
HttpClientTestingModule
],
providers: [
RoleService,
BaseRequestOptions
]
});
- The modules that this particular artifact depends on for functioning are specified in an array of Classes against the import attribute
import {HttpModule} from ‘@angular/http’;
import { HttpClientTestingModule} from ‘@angular/common/http/testing’;
imports: [
HttpModule,
HttpClientTestingModule
],
- HttpClientTestingModule is a module that angular provides and this can be used to expect and flush requests in your tests.
- Jasmine is the testing framework that comes by default with angular
- The following is an example it test
it(‘should be created’, inject([RoleService], (service: RoleService) => {
expect(service).toBeTruthy();
}));
- it method can take an inject function as argument which can be used to inject specific things into the test.
In the example above, the variable “service” is used to inject the RoleService and this service can later be used to run stuff on it
or to run expectations against it.
- inject function can be used to inject multiple things into a test.
- inject function being used to inject multiple things is as below
fit(‘should call the HTTP GET method on url /api/v1/roles/<role-slug>’, inject([RoleService, HttpTestingController], (service: RoleService, httpMock: HttpTestingController) => {
- inject function takes the first argument as an array of Classes and the second argument is a lambda function
- The arguments of the function are the corresponding instances of the Classes that were injected into the array
- e.g. For the first argument [RoleService, HttpTestingController],
the corresponding second argument is (service: RoleService, httpMock: HttpTestingController) => { // function content }
- expect(service).toBeTruthy()
This is the standard format of tests that angular-cli auto generates when ng generate is used to generate the spec files.
- fit(‘should .. ‘, ()={});
There is no it.only() in jasmine library. The equivalent to it.only() is fit()
-xit(‘should…’, ()=>{});
There is no it.skip() in jasmine library. The equivalent to i.skip() is xit()
- To test that your service method has invoked HTTP GET against a specific url, do the following
a) import { HttpClientTestingModule, HttpTestingController, TestRequest } from ‘@angular/common/http/testing’;
b) Configure HttpClientTestingModule as a dependent module in the TestBed using TestBed.configureTestingModule(
imports: [
HttpClientTestingModule
],
)
c) Configure the service that you are wanting to test as a provider in your testing configuration by doing the necessary imports and adding it into the providers section.
import { RoleService } from ‘./role.service’;
TestBed.configureTestingModule({
providers: [
RoleService
]
});
At this point, the necessary configurations are done
d) Now, to run the test, inject the service and the HttpClientTestingModule into your it function.
fit(‘should call the HTTP GET method on url /api/v1/roles/<role-slug>’, inject([RoleService, HttpTestingController], (service: RoleService, httpMock: HttpTestingController) => {
Now, we get into the body of the it function.
e) Note that the function being tested returns an observable. So the observable needs to be subscribed to to get the final result.
f) The reason the service returns Observable is that the HttpClient that it uses returns Observables.
g) The library that returns Observables is HttpClient
import { HttpClient } from ‘@angular/common/http’;
h) Until the subscribe method is called on the get method, the network request does not get sent as observed from the Chrome console.
i) The reason all this context is being given is that for the tests to be run, the subscribe needs to be called and once the subscribe is called, assertions
can be run.
j) The following is what the subscribe run looks like
service.get(‘slug’).subscribe((data: any) => {
expect(data).toEqual(testData);
});
Here, the context is that the service method has been called. Now, we need to run the assertions and assert that the right HTTP calls were made with the right parameters.
k) To assert that a network call went to a particular url, use an instance of HttpTestingController which was injected into the testing it function.
httpMock: HttpTestingController
fit(‘should call the HTTP GET method on url /api/v1/roles/<role-slug>’, inject([RoleService, HttpTestingController], (service: RoleService, httpMock: HttpTestingController) => {
l) HttpTestingControllet.expectOne is the method to test that a particular url was called over HTTP.
const req: TestRequest = httpMock.expectOne(`http://url/whatever`);
m) To run further assertions on the request, the request can be captured in a variable of type TestRequest as in the above line.
n) To assert that the particular method that was used on the url, run code as below.
expect(req.request.method).toEqual(‘GET’);
expect(req.request.method).toEqual(‘PUT’);
expect(req.request.method).toEqual(‘POST’);
o) Lagging slashes should not be missed out when the assertions are run and the test needs to match the code accordingly
httpMock.expectOne(`http://url/whatever`); is different from
httpMock.expectOne(`http://url/whatever/`);
p) To set the behavior that an http method responds with something specific, use TestRequest.flush(<specified object>)
req.flush(testData);
RouterTestingModule is available in @angular/router/testing and can be used to mock router behavior.
import { RouterTestingModule } from ‘@angular/router/testing’;
Specify as an import module while configuring the testing module.
How to test resolve method in Angular?
a) Looks at the code for the resolve
b) The resolve method in the code has two arguments
- ActivatedRouteSnapshot instance and
- RouterStateSnapshot instance
c) The code is as below
resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<Role> {
return this.roleService.get(route.paramMap.get(‘slug’)).take(1).map(role => {
if (role) {
return role;
} else {
this.router.navigateByUrl(‘/roles/list’);
return null;
}
});
d) Create a mock for each of the parameters based on how the calls within it is structured.
const activatedRouteSnapshotStub:any = {
paramMap: {
get: function(){
return “slug-value”;
}
}
};
which corresponds to route.paramMap.get(‘slug’)
route.
paramMap.
get(‘slug’)
e) Note that the type needs to be specified as any so that it can pose as ActivatedRouteSnapshot
f) The state parameter is not actively used in the code. So, the test just needs an empty mock.
const routerStateSnapshotStub:any = {};
g) Now, to assert that the get method within paramMap is called, we use a spy.
The spy comes from the jasmine library which is automatically available in applications generated using angular cli.
h) To declare the spy, use the code as below
let getSpy: jasmine.Spy;
i) To assign the spy to a method, use the spyOn method as below.
getSpy = spyOn(activatedRouteSnapshotStub.paramMap, ‘get’);
j) To assert that the spy was invoked, use the toHaveBeenCalledWith method as below.
expect(getSpy).toHaveBeenCalledWith(“slug”);