Testing a Component
Writing unit tests is very much like flossing. We all agree that we should floss (and write tests) but it is really easy to deprioritize given the general level of discomfort that comes with it. Angular takes the edge off of the proposition of writing tests by providing us with some incredibly handy utilities that allow us to do all sorts of useful things. In this lesson, we are going to walk through how to set up a basic component test using the Angular testing utilities. My personal goal is to illustrate a basic pattern that you can use over and over when writing component tests. The pattern I illustrate in the screencast below was so common that I eventually turned it into a Webstorm live template that you can copy at the end of the lesson to save you time. Enjoy!
The Simple Component
We are going to write a simple component test around a simple component. By starting with a simple component, we can focus on adding in the Angular testing utilities one by one to see how they work. In the component below, we have a template with an h1 tag and a property on our component class called subject. We will use these two items to write tests around once we have our spec set up.
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-simple', template: ' <h1>Hello {{subject}}!</h1> ' }) export class SimpleComponent implements OnInit { subject: string = 'world'; constructor() { } ngOnInit() { } }
Initial Test Structure
The first thing I like to do after writing my initial describe block is to declare the component that I am going to test, the component test fixture, and the component debug element. We will elaborate on the specifics of each item in a moment, but the short version of this setup is that the component fixture holds our component and exposes information that makes it easier to write tests. The debug element is a reference to the component’s template.
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let de: DebugElement; });
The TestBed
With our declarations out of the way, we need to break ground by leveraging the TestBed utility which lays the foundation for all our tests. The TestBed is responsible for configuring and initializing the environment that we are going to write our tests in. By calling TestBed.configureTestingModule, we can set up a special testing module that allows us to test our component. This is very much like defining an ngModule in that we can define declarations, imports, providers, etc that we want to expose to our component. In this case, our simple component can stand alone, so we are only going to define our declarations and pass in SimpleComponent.
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let de: DebugElement; beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }); // Not done yet... });
The Fixture
We use TestBed.configureTestingModule to define the environment that we want our component under test to live in, but we need the ability to get specific information about the component as it lives within our tests. This is where the ComponentFixture comes into play as it gives us a reference to the component instance being tested, as well as the component’s template.
Initialize the Fixture
To instantiate the component fixture, we need to daisy chain one more method onto our test bed, createComponent, and pass in the component we want to create. By attaching .createComponent(SimpleComponent) to the end our of TestBed.configureTestingModule call, we are now receiving a ComponentFixture instance which will satisfy our typings.
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent); });
Get a Component Reference
With our component fixture initialized, we can get a reference to the component we are testing by calling fixture.componentInstance, which we will assign to component.
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent); component = fixture.componentInstance; });
Get a DebugElement Reference
We will also get a reference to the debug element of the component by calling fixture.debugElement.
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent); component = fixture.componentInstance; de = fixture.debugElement; });
Initiate Change Detection
This next step is optional, but I do it by default and then turn it off as needed. When we create a component on the test bed, Angular does not automatically initiate change detection which means that our template will not be rendered with its bindings evaluated. This is so that we have greater control over how we test our component pre-binding and post-binding. This simply means that we need to manually trigger change detection, and thankfully our handy component fixture makes this easy. When we want to trigger change detection, we just call fixture.detectChanges which you can see I have done in the bottom of our beforeEach block.
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); });
Some Basic Tests
Let’s take a moment and walk through a few basic tests so that we can see how to use our newly minted component and debug element in play. First things first, we can test properties and methods that exist on our component instance by calling them directly on our component reference. For instance, we initialize the hello property with the value of world which we can assert in the test below.
it('sets "subject" to "world" by default', () => { expect(component.subject).toBe('world'); });
We can also test how our template renders by using our debug element reference and an additional testing utility called By. Based on our template, we would expect that when it renders, we will have an h1 tag that says Hello world! inside of it. To test this, we will first get a reference to the h1 tag by calling de.query and passing in By.css(‘h1’) which will return a new debug element for the h1 tag. From there, we can assert that h1.nativeElement.innerText is indeed Hello world!.
it('greets the subject', () => { const h1 = de.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello world!'); });
The two previous tests are fairly mild in the “wow factor” department but we can combine them to do something that was actually quite hard in AngularJS 1.x. We are going to change a property on the component class and verify that the component template renders correctly. The first thing we will do is change the subject property to developer and then query our debug element and assert that it renders Hello developer!. Almost there! We are so close!
it('updates the subject', () => { component.subject = 'developer'; // Missing something important const h1 = de.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello developer!'); });
Because we changed a property on the component, we need to initiate change detection so that the template will update. Again, this is solved by calling fixture.detectChanges between our update and our assertion.
it('updates the subject', () => { component.subject = 'developer'; fixture.detectChanges(); const h1 = de.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello developer!'); });
The Full Test
For reference, here is the spec in its entirety.
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let de: DebugElement; beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); }); it('sets "subject" to "world" by default', () => { expect(component.subject).toBe('world'); }); it('greets the subject', () => { const h1 = de.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello world!'); }); it('updates the subject', () => { component.subject = 'developer'; fixture.detectChanges(); const h1 = de.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello developer!'); }); });
There are a bunch of other techniques and topics we could talk about around testing, but understanding how to use the Angular testing utilities to write a basic component test is an 80/20 move. You will find yourself configuring your test module, instantiating your component fixture, getting a component and debug element reference, and manually kicking off change detection over and over. Eventually, you will develop muscle memory and these foundational techniques will feel like old friends.
Bonus
I recommend typing out all your code until you are comfortable with every piece of it and the purpose that it serves. As you progress in your familiarity, you will start to notice a common theme that remains even as we introduce variations. It is usually at this point that I extract the common pieces and turn it into a live template or snippet so that I do not have to type it over and over. Here is the live template that I use in Webstorm when writing component tests – it’s a real time saver! Feel free to use this in your own projects and if you come up with any interesting variations, let me know!
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DebugElement } from '@angular/core'; describe('$COMPONENT$', () => { let component: $COMPONENT$$END$; let fixture: ComponentFixture<$COMPONENT$>; let de: DebugElement; beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [$COMPONENT$] }).createComponent($COMPONENT$); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); }); });
Resources
Angular 2 – Testing Guide – Awesome guide from my buddy Gerard Sans
Thank you very much, fantastically clear! You convinced me to floss every day! I will write my first unit test today and I will let you know how it goes.