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.