Unit tests are the bread and butter of your application testing process. They exist as a companion to your source code, and most of the time, the bulk of your application tests will be unit tests. They are lightweight, run quickly, are easy to read and reason about, and can give context as to how the code should be used and how it might behave.
Setting up Karma, Jasmine, TypeScript, and Angular 2 along with all the connecting configurations between them is a bit of an imposing task; it was deemed to be out of the scope of this chapter. It's not a very interesting discussion to get all of them to work together, especially since there are already so many example projects that have put together their own setups for you. It's far more interesting to dive directly into the tests themselves and see how they can actually interact with Angular 2.
The code, links, and a live example related to this recipe are available at .
This recipe will assume you are using a working Angular 2 testing environment. The one provided in the application generated by the Angular CLI is ideal. Tests can be run in this environment with the following command inside the project directory:
ng test
Begin with the following component:
[src/app/article/article.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'app-article', template: ` <h1> {{ title }} </h1> ` }) export class ArticleComponent { title: string = 'Captain Hook Sues Over Spork Snafu'; }
Your goal is to entirely flesh out article.component.spec.ts
to test this class.
The simplest possible test you can think of is the one that will simply check that you are able to instantiate an instance of ArticleComponent
. Begin with that test:
[src/app/article/article.component.spec.ts] import {ArticleComponent} from './article.component'; describe('Component: Article', () => { it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); });
Nothing tricky is going on here. Since ArticleComponent
is just a plain old TypeScript class, nothing is preventing you from creating an instance and inspecting it in the memory.
However, for it to actually behave like an Angular 2 component, you'll need some other tools.
When you try to puppet an Angular 2 environment for the component in a test, there are a number of considerations you'll need to account for. First, Angular 2 unit tests heavily rely upon TestBed
, which can be thought of as your testing multitool.
The denomination of unit tests when dealing with a component involves ComponentFixture
. TestBed.createComponent()
will create a fixture wrapping an instance of the desired component.
The need for fixtures is centered in how unit tests are supposed to work. An ArticleComponent
does not make sense when instantiated as it was with the initial test you wrote. There is no DOM element to attach to, no running application, and so on. It doesn't make sense for the component unit tests to have an explicit dependency on these things. So, ComponentFixture
is Angular's way of letting you test only the concerns of the component as it would normally exist, without worrying about all the messiness of its innate dependencies.
The TestBed
fixture's asynchronous behavior mandates that the test logic is executed inside an async()
wrapper.
The async()
wrapper simply runs the test inside its own zone. This allows the test runner to wait for all the asynchronous calls inside the test to complete them before ending the test.
Begin by importing TestBed
and async
from the Angular testing module and put together the skeleton for two more unit tests:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); it('should have correct title', async(() => { })); it('should render title in an h1 tag', async(() => { })); });
Now that you have the skeletons for the two tests you'd like to write, it's time to use TestBed
to define the test module. Angular 2 components are paired with a module definition, but when performing unit tests, you'll need to use the TestBed
module's definition for the component to work properly. This can be done with TestBed.configureTestModule()
, and you'll want to invoke this before each test.
Jasmine's describe
allows you to group beforeEach
and afterEach
inside it, and it is perfect for use here:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); describe('Async', () => { beforeEach( () => { TestBed.configureTestingModule({ declarations: [ ArticleComponent ], }); }); it('should have correct title', async(() => { })); it('should render title in an h1 tag', async(() => { })); }); });
TestBed
gives you the ability to create a fixture, but you have yet to actually do it. You'll need a fixture for both the async
tests, so it makes sense to do this in beforeEach
too:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { let fixture; it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); describe('Async', () => { beforeEach( () => { TestBed.configureTestingModule({ declarations: [ ArticleComponent ], }); fixture = TestBed.createComponent(ArticleComponent); })); afterEach(() => { fixture = undefined; }); it('should have correct title', async(() => { })); it('should render title in an h1 tag', async(() => { })); }); });
Here, fixture
is assigned to undefined
in the afterEach
teardown. This is technically superfluous for the purpose of these tests, but it is good to get into the habit of performing a robust teardown of shared variables in unit tests. This is because one of the most frustrating things to debug in a test suite is test variable bleed. After all, these are just functions running in a sequence in a browser.
Now that the fixture is defined for each test, you can use its methods to inspect the instantiated component in different ways.
For the first test, you'd like to inspect the ArticleComponent
object itself from within ComponentFixture
. This is exposed with the componentInstance
property:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { let expectedTitle = 'Captain Hook Sues Over Spork Snafu'; let fixture; it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); describe('Async', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ArticleComponent ], }); fixture = TestBed.createComponent(ArticleComponent); })); afterEach(() => { fixture = undefined; }); it('should have correct title', async(() => { expect(fixture.componentInstance.title) .toEqual(expectedTitle); })); it('should render title in an h1 tag', async(() => { })); }); });
For the second test, you want access to the DOM that the fixture has attached the component instance to. The root element that the component is targeting is exposed with the nativeElement
property:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { let expectedTitle = 'Captain Hook Sues Over Spork Snafu'; let fixture; it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); describe('Async', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ArticleComponent ], }); fixture = TestBed.createComponent(ArticleComponent); })); afterEach(() => { fixture = undefined; }); it('should have correct title', async(() => { expect(fixture.componentInstance.title) .toEqual(expectedTitle); })); it('should render title in an h1 tag', async(() => { expect(fixture.nativeElement.querySelector('h1') .textContent).toContain(expectedTitle); })); }); });
If you run these tests, you will notice that the last test will fail. The test sees an empty string inside <h1></h1>
. This is because you are binding a value in the template to a component member. Since the fixture controls the entire environment surrounding the component, it also controls the change detection strategy—which, here, is to not run until it is told to do so. You can trigger a round of change detection using the detectChanges()
method:
[src/app/article/article.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {ArticleComponent} from './article.component'; describe('Component: Article', () => { let expectedTitle = 'Captain Hook Sues Over Spork Snafu'; let fixture; it('should create an instance', () => { let component = new ArticleComponent(); expect(component).toBeTruthy(); }); describe('Async', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ArticleComponent ], }); fixture = TestBed.createComponent(ArticleComponent); })); afterEach(() => { fixture = undefined; }); it('should have correct title', async(() => { expect(fixture.componentInstance.title) .toEqual(expectedTitle); })); it('should render title in an h1 tag', async(() => { fixture.detectChanges(); expect(fixture.nativeElement.querySelector('h1') .textContent).toContain(expectedTitle); })); }); });
With this, you should see Karma run and pass all three tests.
When it comes to testing components, fixture is your friend. It gives you the ability to inspect and manipulate the component in an environment that it will behave comfortably in. You are then able to manipulate the instances of input made to the component, as well as inspect their output and resultant behavior.
This is the core of unit testing: the "thing" you are testing—here, a component class—should be treated as a black box. You control what goes into the box, and your tests should measure and define what they expect to come out of the box. If the tests account for all the possible cases of input and output, then you have achieved 100 percent unit test coverage of that thing.