Testing Angular 2 apps — Part 2: Dependency Injection and Components
If you missed part 1 I encourage you to take a look at it here.
Note: Article is based on Angular 2 beta.2
Up-to-date Gist with examples for whole series is here: Angular 2 testing gist
Starter project with setup for tests is here: https://github.com/wkwiatek/angular2-webpack2
Testing Services
The easiest thing to start testing real Angular 2 thing is Service. It is pure JS class which is about to be injected.
Let’s start with simple service:
export class TestService {
public name: string = 'Injected Service';
}
We can go ahead and use something you’ve learned in the previous part:
import {TestService} from './test.service';describe('TestService', () => { beforeEach(function() {
this.testService = new TestService();
}); it('should have name property set', function() {
expect(this.testService.name).toBe('Injected Service');
});});
There it is. Pure service tests can look just like that! But what about injecting them? If you do something like this in your bootstrap function:
bootstrap(App, [TestService]);
then your TestService
is ready to be injected through the whole App
. It means every time when something needs that service we can simply use it.
Things get a little bit more complicated when we want to test such a behaviour. First of all we won’t use pure Jasmine functions any more. This is what is about to happen:
import {
beforeEach,
beforeEachProviders,
describe,
expect,
it,
inject,
injectAsync
} from 'angular2/testing';
import {TestService} from './service';
Note: To run tests in the browser one more thing needs to be included. It’s DOM adapter. It looks like this:
import {setBaseTestProviders} from 'angular2/testing'; import { TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS } from 'angular2/platform/testing/browser'; setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);
What’s in there? Note that all of the helpers from Jasmine were replaced by their equivalents from angular2/testing
. Why? Because Angular 2 relies heavily on Dependency Injection (great article on that topic) and this is not something that Jasmine is aware of. Angular team made wrappers which add their own logic (see source code). The outcome is we can now use inject()
instead of anonymous callback function in it
or beforeEach
that will inject some class.
Note: Angular 2 will eventually use
@Inject
decorator instead ofinject
function but now it’s matter of Traceur limitation.
So to inject something we can say hello to beforeEachProviders
and inject
:
describe('TestService', () => { beforeEachProviders(() => [TestService]); it('should have name property set', inject([TestService], (testService: TestService) => {
expect(testService.name).toBe('Injected Service');
}));});
Here it is! We’ve injected our service into test case. beforeEachProviders
is a way of injecting providers that were originally specified in bootstrap.
Note:
testService: TestService
is only type checking here unlike injection in Component.
Mocking providers
We haven’t solved any problem yet but it’s about to come! Imagine you’d like to mock the service now. It’s common pattern to make a testing environment independent from real implementation of service.
class MockTestService {
public mockName: string = 'Mocked Service';
}describe('TestService', () => { beforeEachProviders(() => [
provide(TestService, {useClass: MockTestService})
]); it('should have name property set', inject([TestService], (testService: TestService) => {
expect(testService.mockName).toBe('Mocked Service');
}));});
What’s cool about that? In the test case code doesn’t know what exactly is TestService
. It doesn’t care – it’s completely transparent. This way we can test the behaviour of the component itself, not its dependencies. Moreover, we can use something from both worlds – the real one and the mocked (if really needed). How? Using simple JS inheritance:
class MockTestService extends TestService {
public sayHello(): string {
return this.name;
}
}describe('TestService', () => { beforeEachProviders(() => [
provide(TestService, {useClass: MockTestService})
]); it('should say hello with name', inject([TestService], (testService: TestService) => {
expect(testService.sayHello()).toBe('Injected Service');
}));});
Component test with DOM changes
Ok. Injecting services themselves to test suites is not what you really need. They can be tested using pure classes (unless you want to inject service into service). The point is that the knowledge gives you ability to fully test a component! But leave it for a while. Go to something different for now.
Let’s say that our component is a list of items (e.g. users). It’s a dumb component — all it does is rendering list for given input. It can’t be simpler than that:
import {Component, Input} from 'angular2/core';
import {NgFor} from 'angular2/common';@Component({
selector: 'list',
template: '<span *ngFor="#user of users">{{user}}</span>',
directives: [NgFor]
})
export class ListComponent {
@Input() public users: Array<string> = [];
}
And now we have to meet a little bit of a ‘magic’ to create such component in tests:
describe('ListComponent', () => { it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
const element = componentFixture.nativeElement;
componentFixture.componentInstance.users = ['John'];
componentFixture.detectChanges();
expect(element.querySelectorAll('span').length).toBe(1);
});
}));});
Note: Do you remember about
BrowserDomAdapter
to make your test work?
First of all look at TestComponentBuilder
. It will create component from given class with decorator and come back with created component as a fixture. We also use injectAsync
there and adds return
to handle asynchronous component creation. We are now able to perform operations on its properties, methods etc. and moreover – we are able to check if Angular specific stuff is happening. What am I talking about? We now have instance of component so we can check everything what is connected with it. One of the valuable things is ability to check if renders what’s proper data.
Note: We don’t care whether property is passed via
@Input()
or not. Change detection has to be run manually so we assume angular will take care of it. It can be a problem if we’re setting different detection strategy.
Now to get fully functional component we miss one thing — DI for component.
Component test with DOM and DI
We have covered these topics alone. Now the point is to combine both to get all you need to test most of the components. So let’s use values of injected service instead of @Input()
. There goes the service:
export class UserService {
public users: Array<string> = ['John'];
}
And the component:
import {Component, Input} from 'angular2/core';
import {NgFor} from 'angular2/common';@Component({
selector: 'list',
template: '<span *ngFor="#user of users">{{user}}</span>',
directives: [NgFor]
})
export class ListComponent {
private users: Array<string> = []; constructor(userService: UserService) {
this.users = userService.users;
}
}
If we run the following test now it’ll throw an error:
describe('ListComponent', () => { it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(ListComponent).then((componentFixture: ComponentFixture) => {
const element = componentFixture.nativeElement;
componentFixture.detectChanges();
expect(element.querySelectorAll('span').length).toBe(1);
});
}));});
There’s no boostrap
in tests so we have to mock dependencies to pass it!
Author of the post: Wojciech Kwiatek
This post was originally published on LiveChat Developer Blog.