So as the enthusiastic new comer, you've decided to roll your sleeves 💪 and you're up to add more unit tests. In this article, I'd like to share the fun of seeing the code coverage percentage increased 📊 📈 in your angular application.
I love angular.io documentation. Really great content and since it is part of angular repo it's well maintained an kept up-to-date. To start with I recommend you reading Testing Advanced cookbook.
When starting testing a #UnitTestLackingApplication, I think tackling Angular Services are easier to start with. Why? Some services might be self contained object without dependencies, or (more frequently the only dependencies might be with http module) and for sure, there is no DOM testing needed as opposed to component testing.
Setting up tests
I'll use the code base of openshift.io to illustrate this post. It's a big enough project to go beyond the getting started apps. Code source could be found in: https://github.com/fabric8io/fabric8-ui/
From my previous post "Debug your Karma", you know how to run unit test and collect code coverage from Istanbul set-up. Simply run:
npm test // to run all test
npm run test:unit // to run only unit test (the one we'll focus on)
npm run test:debug // to debug unit test
When starting a test you'll need:
- to have an entry point, similar to having a
main.ts
which will callTestBed.initTestEnvironment
, this is done once for all your test suite. See spec-bundle.js for a app generated using AngularClass starter. - you also need a "root" module similar (called testing module) to a root module for your application. You'll do it by using
TestBed.configureTestingModule
. This is something to do for each test suite dependent on what you want to test.
Dependency Injection
The DI in Angular consists of:
- Injector - The injector object that exposes APIs to us to create instances of dependencies. In your case we'll use TestBest which inherits from Injector.
- Provider - A provider takes a token and maps that to a factory function that creates an object.
- Dependency - A dependency is the type of which an object should be created.
@Injectable()
export class CodebasesService {
...
constructor(
private http: Http,
private logger: Logger,
private auth: AuthenticationService,
private userService: UserService,
@Inject(WIT_API_URL) apiUrl: string) {
...
}
The service depends on 4 services and one configuration string which is injected. As we want to test in isolation the service we're going to mock most of them.
How to inject mock TestBed.configureTestingModule
I choose to use Logger (no mock) as the service is really simple, I mock Http service (more on that later) and I mock AuthenticationService and UserService using Jasmine spy. Eventually I also inject the service under test CodebasesService.
beforeEach(() => {
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
mockUserService = jasmine.createSpy('UserService');
TestBed.configureTestingModule({
providers: [
Logger,
BaseRequestOptions,
MockBackend,
{
provide: Http,
useFactory: (backend: MockBackend,
options: BaseRequestOptions) => new Http(backend, options),
deps: [MockBackend, BaseRequestOptions]
},
{
provide: AuthenticationService,
useValue: mockAuthService
},
{
provide: UserService,
useValue: mockUserService
},
{
provide: WIT_API_URL,
useValue: "http://example.com"
},
CodebasesService
]
});
});
One thing important to know is that with DI you are not in control of the singleton object created by the framework. This is the Hollywood concept: don't call me, I'll call you. That's why to get the singleton instance created for
CodebasesService
and MockBackend
, you need to get it from the injector either using inject
as below:
beforeEach(inject(
[CodebasesService, MockBackend],
(service: CodebasesService, mock: MockBackend) => {
codebasesService = service;
mockService = mock;
}));
or using
TestBed.get
:
beforeEach(() => {
codebasesService = TestBed.get(CodebasesService);
mockService = TestBed.get(MockBackend);
});
To be or not to be
Notice how you get the instance created for you from the injector
TestBed
. What about the mock instance you provided with useValue
? Is it the same object instance that is being used? Interesting enough if your write a test like:
it('To be or not to be', () => {
let mockAuthServiceFromDI = TestBed.get(AuthenticationService);
expect(mockAuthService).toBe(mockAuthServiceFromDI); // [1]
expect(mockAuthService).toEqual(mockAuthServiceFromDI); // [2]
});
line 1 will fail whereas line 2 will succeed. Jasmine uses
toBe
to compare object instance whereas toEqual
to compare object's values. As noted in Angular documentation, the instances created by the injector are not the ones you used for the provider factory method. Always, get your instance from the injector ie: TestBed.
Mocking Http module to write your test
Using HttpModule in TestBed
Let's revisit our TestBed's configuration to use
HttpModule
:
beforeEach(() => {
mockLog = jasmine.createSpyObj('Logger', ['error']);
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
mockUserService = jasmine.createSpy('UserService');
TestBed.configureTestingModule({
imports: [HttpModule], // line [1]
providers: [
Logger,
{
provide: XHRBackend, useClass: MockBackend // line [2]
},
{
provide: AuthenticationService,
useValue: mockAuthService
},
{
provide: UserService,
useValue: mockUserService
},
{
provide: WIT_API_URL,
useValue: "http://example.com"
},
CodebasesService
]
});
codebasesService = TestBed.get(CodebasesService);
mockService = TestBed.get(XHRBackend);
});
By adding an
HttpModule
to our testing module in line [1], the providers for Http, RequestOptions
is already configured. However, using an NgModule’s providers property, you can still override providers (line 2) even though it has being introduced by other imported NgModules.
With this second approach we can simply override XHRBackend.
Mock http response
Using Jasmine DBB style, let's test the
addCodebase method
:
it('Add codebase', () => {
// given
const expectedResponse = {"data": githubData};
mockService.connections.subscribe((connection: any) => {
connection.mockRespond(new Response(
new ResponseOptions({
body: JSON.stringify(expectedResponse),
status: 200
})
));
});
// when
codebasesService.addCodebase("mySpace", codebase).subscribe((data: any) => {
// then
expect(data.id).toEqual(expectedResponse.data.id);
expect(data.attributes.type).toEqual(expectedResponse.data.attributes.type);
expect(data.attributes.url).toEqual(expectedResponse.data.attributes.url);
});
});
Let's do our testing using the well-know given, when, then paradigm.
We start with given: Angular’s http module comes with a testing class MockBackend. No http request is sent and you have an API to mock your call. Using
connection.mockResponse
we can mock the response of any http call. We can also mock failure (a must-have to get a 100% code coverage 😉) with connection.mockError
.
The when is simply about calling our addCodebase method.
The then is about verifying the expected versus the actual result. Because http call return RxJS Observable, very often service's method that use async REST call will use Observable too. Here our addCodebase method return a
Observable
. To be able to unwrap the Observable use the subscribe method. Inside it you can access the Codebase object and compare its result.What's next?
In this post you saw how to test a angular service using http module. You can get the full source code in github.
You've seen how to set-up a test with Dependency Injection, how to mock http layer and how to write your jasmine test. Next blog post, we'll focus on UI layer and how to test angular component.
Tweet
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.