Update:
This post was written when Angular was in beta. Since then the testing tooling has changed significantly. Updated documentation can be found at https://angular.io/guide/testing
Among the many new and exciting features of Angular 2 comes a robust and completely integrated testing module based off of the Jasmine testing framework. Using angular2/testing makes writing unit tests for Angular 2 components and services a lot easier. In this post, we'll explore how to get started writing unit tests for components using the TestComponentBuilder. You should already have a basic understanding of how unit tests are written and some experience writing Angular 1.x tests.
Testing Components
Components are made up of classes, and our testing strategy revolves around verifying the correctness of the properties and methods of those classes. In Angular 2, a component's class will often rely on some dependencies - a service, pipe, directive, etc. When writing unit tests for components we don't actually bootstrap the application, instead, we initialize the component and inject any dependencies manually. Once we have an instance of our component we can call its methods, check the values of its properties, and query whatever output it has made to the DOM. Pretty straightforward right? Let's take a look at a basic component that we will write some unit tests for.
import {QuoteService} from './quote.service';
import {Component} from 'angular2/core';
@Component({
selector: 'my-quote',
template: `
<h3>Random Quote</h3>
<div>{{quote}}</div>
`
})
export class QuoteComponent {
quote: string;
constructor (private quoteService: QuoteService){}
getQuote() {
this.quote = this.quoteService.getQuote();
}
}
This component, QuoteComponent, will simply display a random quote that it gets from the QuoteService. What exactly do we want to test here? Well, we want to verify that calling the getQuote() method will get a new quote, and then place it in the DOM, right below the <h3> heading.
Initializing Components for Testing
First, let's see how we initialize QuoteComponent and provide it with a mocked version of QuoteService. Why do we not use the actual QuoteService? Well, we want to separate our testing concerns and only concentrate on verifying the behaviour of our component. Testing QuoteService is an entirely different operation.
import {
expect,
it,
describe,
injectAsync,
TestComponentBuilder,
beforeEachProvider
} from 'angular2/testing';
import {provide} from 'angular2/core';
describe('Testing Quote Component', () => {
beforeEachProvider(() => {
provide(QuoteService, {useClass: MockQuoteService})
});
it('should get quote', injectAsync(
[TestComponentBuilder], (tcb) => {
return tcb.createAsync(QuoteComponent).then((fixture) => {
});
}
));
});
Some of this may seem familiar - we use describe to encapsulate what it is we are testing and use it to formulate a scenario.
The first step in initialzing the QuoteComponent is providing it its sole dependency - QuoteService. By using the beforeEachProvider hook, which is the first thing executed in the test, we can manually configure the dependency injection system to use the QuoteService by calling provide. Since we want to use a mocked version of QuoteService, we can override any instance of QuoteService with our mocked version, MockQuoteService, by giving provide the class we want to use instead.
Here is what our MockQuoteService looks like:
class MockQuoteService {
public quote: string = 'Test quote';
getQuote() {
return this.quote;
}
}
Since our QuoteComponent makes a call to getQuote we need to include an implementation of it. We also include a quote - "Test quote", so we know that if that specific string doesn't show up in our QuoteComponent, something is not working correctly.
After we have provided QuoteComponent with its dependencies, we need to create an instance of it, for this, we use the helper class TestComponentBuilder. The TestComponentBuilder will create a testing fixture, which is the primary object we will interface with when performing all our tests relevant to the functionality of the component. To get access to an instance of the TestComponentBuilder we use injectAsync to inject it into our test - asynchronously. Using the TestComponentBuilder we create a new testing fixture by calling createAsync and passing in the component we want a fixture of. Since all this is being done asynchronously, we return the Promise from createAsync to know when the test has completed.
What About Dependencies Defined in Annotations?
Often a components dependencies will come from its annotation definition, like this one - ExampleComponent:
@Component({
providers: [myCustomProvider],
directives: [myCustomDirective],
...
})
Here we can't use beforeEachProvider to inject our dependencies. Instead, we must use the overrideProviders and overrideDirectives methods in the TestComponentBuilder helper object before our component fixture is created. This might look a little something like this:
...
it('should fulfill dependencies', injectAsync(
[TestComponentBuilder], (tcb) => {
return tcb
.overrideProviders(
ExampleComponent,
[provide(myCustomProvider, {useClass: MockMyCustomProvider})]
)
.overrideDirectives(
ExampleComponent, myCustomDirective, MockMyCustomDirective
)
.createAsync(QuoteComponent).then((fixture) => {
});
}
));
...
Whew! Thats a lot to take in, but we got our QuoteComponent all set up with mocked out dependencies, now we are ready for some actually tests!
Running Tests Against Components
Now that we have an instance of QuoteComponent as a testing fixture, lets call the getQuote method and see what happens
...
it('should get quote', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(QuoteComponent).then(fixture) => {
fixture.debugElement.componentInstance.getQuote();
fixture.detectChanges();
var compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('div')).toHaveText('Test Quote');
}
}));
...
Using the QuoteComponent fixture, we can retrieve access to the underlying component instance through the debugElement.componentInstance property path. From that instance, we call getQuote.
Then, using the fixture we call detectChanges to manually trigger the change detection mechanism to have angular sync our component instance property this.quote with its template <div>{{quote}}</div>
Now, when we want to check what changes did occur, we get access to the native elements (in this case the DOM) through the debugElement.nativeElement property path. This will give us an object that we can probe for whatever piece of data we are expecting to find. Using expect we focus in on a particular element we want to test - in this, case the sole <div> tag. Then, we call toHaveText to see if that <div> tag contains the quote we mocked into our MockQuoteService - "Test Quote". Our test will pass if our test quote appears in the div tag, and it will fail if it does not.
And there you have it, we've successfully written a unit test that covers the functionality of our QuoteComponent!
Conclusion
Congratulations! You now know how to put together a unit test for Angular 2 components. Fulfilling dependencies, mocking out data, and working with testing fixtures is soon to be second nature! Jump over to the Jasmine docs to explore other checks you can use in your testing scripts, as well as the Angular 2 API to see how you can use a testing fixture in different ways. Also, check back in for part 2 of this post!
Continue on, to read part 2 of this series.