Testing
This guide offers tips and techniques for testing Angular applications. Though this page includes some general testing principles and techniques, the focus is on testing applications written with Angular.
Live examples
This guide presents tests of a sample application that is much like the Tour of Heroes tutorial. The sample application and all tests in this guide are available as live examples for inspection, experiment, and download:
- A spec to verify the test environment.
- The first component spec with inline template.
- A component spec with external template.
- The QuickStart seed's AppComponent spec.
- The sample application to be tested.
- All specs that test the sample application.
- A grab bag of additional specs.
Introduction to Angular Testing
This page guides you through writing tests to explore and confirm the behavior of the application. Testing does the following:
-
Guards against changes that break existing code (“regressions”).
-
Clarifies what the code does both when used as intended and when faced with deviant conditions.
-
Reveals mistakes in design and implementation. Tests shine a harsh light on the code from many angles. When a part of the application seems hard to test, the root cause is often a design flaw, something to cure now rather than later when it becomes expensive to fix.
Tools and technologies
You can write and run Angular tests with a variety of tools and technologies. This guide describes specific choices that are known to work well.
Technology | Purpose |
---|---|
Jasmine | The Jasmine test framework provides everything needed to write basic tests. It ships with an HTML test runner that executes tests in the browser. |
Angular testing utilities | Angular testing utilities create a test environment for the Angular application code under test. Use them to condition and control parts of the application as they interact within the Angular environment. |
Karma | The karma test runner is ideal for writing and running unit tests while developing the application. It can be an integral part of the project's development and continuous integration processes. This guide describes how to set up and run tests with karma. |
Protractor | Use protractor to write and run end-to-end (e2e) tests. End-to-end tests explore the application as users experience it. In e2e testing, one process runs the real application and a second process runs protractor tests that simulate user behavior and assert that the application respond in the browser as expected. |
Setup
There are two fast paths to getting started with unit testing.
-
Start a new project following the instructions in Setup.
-
Start a new project with the Angular CLI.
Both approaches install npm packages, files, and scripts pre-configured for applications built in their respective modalities. Their artifacts and procedures differ slightly but their essentials are the same and there are no differences in the test code.
In this guide, the application and its tests are based on the setup instructions. For a discussion of the unit testing setup files, see below.
Isolated unit tests vs. the Angular testing utilities
Isolated unit tests examine an instance of a class all by itself without any dependence on Angular or any injected values. The tester creates a test instance of the class with new
, supplying test doubles for the constructor parameters as needed, and then probes the test instance API surface.
You should write isolated unit tests for pipes and services.
You can test components in isolation as well. However, isolated unit tests don't reveal how components interact with Angular. In particular, they can't reveal how a component class interacts with its own template or with other components.
Such tests require the Angular testing utilities. The Angular testing utilities include the TestBed
class and several helper functions from @angular/core/testing
. They are the main focus of this guide and you'll learn about them when you write your first component test. A comprehensive review of the Angular testing utilities appears later in this guide.
But first you should write a dummy test to verify that your test environment is set up properly and to lock in a few basic testing skills.
The first karma test
Start with a simple test to make sure that the setup works properly.
Create a new file called 1st.spec.ts
in the application root folder, src/app/
Tests written in Jasmine are called specs . The filename extension must be
.spec.ts
, the convention adhered to bykarma.conf.js
and other tooling.
Put spec files somewhere within the src/app/
folder. The karma.conf.js
tells karma to look for spec files there, for reasons explained below.
Add the following code to src/app/1st.spec.ts
.
describe('1st tests', () => { it('true is true', () => expect(true).toBe(true)); });
Run with karma
Compile and run it in karma from the command line using the following command:
npm test
The command compiles the application and test code and starts karma. Both processes watch pertinent files, write messages to the console, and re-run when they detect changes.
The documentation setup defines the
test
command in thescripts
section of npm'spackage.json
. The Angular CLI has different commands to do the same thing. Adjust accordingly.
After a few moments, karma opens a browser and starts writing to the console.
Hide (don't close!) the browser and focus on the console output, which should look something like this:
> npm test ... [0] 1:37:03 PM - Compilation complete. Watching for file changes. ... [1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS Chrome 51.0.2704: Executed 1 of 1 SUCCESS SUCCESS (0.005 secs / 0.005 secs)
Both the compiler and karma continue to run. The compiler output is preceded by [0]
; the karma output by [1]
.
Change the expectation from true
to false
.
The compiler watcher detects the change and recompiles.
[0] 1:49:21 PM - File change detected. Starting incremental compilation... [0] 1:49:25 PM - Compilation complete. Watching for file changes.
The karma watcher detects the change to the compilation output and re-runs the test.
[1] Chrome 51.0.2704 1st tests true is true FAILED [1] Expected false to equal true. [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs)
It fails of course.
Restore the expectation from false
back to true
. Both processes detect the change, re-run, and karma reports complete success.
The console log can be quite long. Keep your eye on the last line. When all is well, it reads
SUCCESS
.
Test debugging
Debug specs in the browser in the same way that you debug an application.
- Reveal the karma browser window (hidden earlier).
- Click the DEBUG button; it opens a new browser tab and re-runs the tests.
- Open the browser's “Developer Tools” (
Ctrl-Shift-I
on windows;Command-Option-I
in OSX). - Pick the "sources" section.
- Open the
1st.spec.ts
test file (Control/Command-P, then start typing the name of the file). - Set a breakpoint in the test.
- Refresh the browser, and it stops at the breakpoint.
Try the live example
You can also try this test as a in plunker. All of the tests in this guide are available as live examples.
Test a component
An Angular component is the first thing most developers want to test. The BannerComponent
in src/app/banner-inline.component.ts
is the simplest component in this application and a good place to start. It presents the application title at the top of the screen within an <h1>
tag.
import { Component } from '@angular/core'; @Component({ selector: 'app-banner', template: '<h1>{{title}}</h1>' }) export class BannerComponent { title = 'Test Tour of Heroes'; }
This version of the BannerComponent
has an inline template and an interpolation binding. The component is probably too simple to be worth testing in real life but it's perfect for a first encounter with the Angular testing utilities.
The corresponding src/app/banner-inline.component.spec.ts
sits in the same folder as the component, for reasons explained in the FAQ answer to "Why put specs next to the things they test?".
Start with ES6 import statements to get access to symbols referenced in the spec.
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { BannerComponent } from './banner-inline.component';
Here's the describe
and the beforeEach
that precedes the tests:
describe('BannerComponent (inline template)', () => { let comp: BannerComponent; let fixture: ComponentFixture<BannerComponent>; let de: DebugElement; let el: HTMLElement; beforeEach(() => { TestBed.configureTestingModule({ declarations: [ BannerComponent ], // declare the test component }); fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // query for the title <h1> by CSS element selector de = fixture.debugElement.query(By.css('h1')); el = de.nativeElement; }); });
TestBed
TestBed
is the first and most important of the Angular testing utilities. It creates an Angular testing module—an @NgModule
class—that you configure with the configureTestingModule
method to produce the module environment for the class you want to test. In effect, you detach the tested component from its own application module and re-attach it to a dynamically-constructed Angular test module tailored specifically for this battery of tests.
The configureTestingModule
method takes an @NgModule
-like metadata object. The metadata object can have most of the properties of a normal NgModule.
This metadata object simply declares the component to test, BannerComponent
. The metadata lack imports
because (a) the default testing module configuration already has what BannerComponent
needs and (b) BannerComponent
doesn't interact with any other components.
Call configureTestingModule
within a beforeEach
so that TestBed
can reset itself to a base state before each test runs.
The base state includes a default testing module configuration consisting of the declarables (components, directives, and pipes) and providers (some of them mocked) that almost everyone needs.
The testing shims mentioned later initialize the testing module configuration to something like the
BrowserModule
from@angular/platform-browser
.
This default configuration is merely a foundation for testing an app. Later you'll call TestBed.configureTestingModule
with more metadata that define additional imports, declarations, providers, and schemas to fit your application tests. Optional override
methods can fine-tune aspects of the configuration.
createComponent
After configuring TestBed
, you tell it to create an instance of the component-under-test. In this example, TestBed.createComponent
creates an instance of BannerComponent
and returns a component test fixture.
Do not re-configure
TestBed
after callingcreateComponent
.
The createComponent
method closes the current TestBed
instance to further configuration. You cannot call any more TestBed
configuration methods, not configureTestingModule
nor any of the override...
methods. If you try, TestBed
throws an error.
ComponentFixture
, DebugElement
, and query(By.css)
The createComponent
method returns a ComponentFixture
, a handle on the test environment surrounding the created component. The fixture provides access to the component instance itself and to the DebugElement
, which is a handle on the component's DOM element.
The title
property value is interpolated into the DOM within <h1>
tags. Use the fixture's DebugElement
to query
for the <h1>
element by CSS selector.
The query
method takes a predicate function and searches the fixture's entire DOM tree for the first element that satisfies the predicate. The result is a different DebugElement
, one associated with the matching DOM element.
The
queryAll
method returns an array of allDebugElements
that satisfy the predicate.A predicate is a function that returns a boolean. A query predicate receives a
DebugElement
and returnstrue
if the element meets the selection criteria.
The By
class is an Angular testing utility that produces useful predicates. Its By.css
static method produces a standard CSS selector predicate that filters the same way as a jQuery selector.
Finally, the setup assigns the DOM element from the DebugElement
nativeElement
property to el
. The tests assert that el
contains the expected title text.
The tests
Jasmine runs the beforeEach
function before each of these tests
it('should display original title', () => { fixture.detectChanges(); expect(el.textContent).toContain(comp.title); }); it('should display a different test title', () => { comp.title = 'Test Title'; fixture.detectChanges(); expect(el.textContent).toContain('Test Title'); });
These tests ask the DebugElement
for the native HTML element to satisfy their expectations.
detectChanges
: Angular change detection within a test
Each test tells Angular when to perform change detection by calling fixture.detectChanges()
. The first test does so immediately, triggering data binding and propagation of the title
property to the DOM element.
The second test changes the component's title
property and only then calls fixture.detectChanges()
; the new value appears in the DOM element.
In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke or an asynchronous activity (e.g., AJAX) completes.
The TestBed.createComponent
does not trigger change detection. The fixture does not automatically push the component's title
property value into the data bound element, a fact demonstrated in the following test:
it('no title in the DOM until manually call `detectChanges`', () => { expect(el.textContent).toEqual(''); });
This behavior (or lack of it) is intentional. It gives the tester an opportunity to inspect or change the state of the component before Angular initiates data binding or calls lifecycle hooks.
Try the live example
Take a moment to explore this component spec as a and lock in these fundamentals of component unit testing.
Automatic change detection
The BannerComponent
tests frequently call detectChanges
. Some testers prefer that the Angular test environment run change detection automatically.
That's possible by configuring the TestBed
with the ComponentFixtureAutoDetect
provider. First import it from the testing utility library:
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
Then add it to the providers
array of the testing module configuration:
TestBed.configureTestingModule({ declarations: [ BannerComponent ], providers: [ { provide: ComponentFixtureAutoDetect, useValue: true } ] })
Here are three tests that illustrate how automatic change detection works.
it('should display original title', () => { // Hooray! No `fixture.detectChanges()` needed expect(el.textContent).toContain(comp.title); }); it('should still see original title after comp.title change', () => { const oldTitle = comp.title; comp.title = 'Test Title'; // Displayed title is old because Angular didn't hear the change :( expect(el.textContent).toContain(oldTitle); }); it('should display updated title after detectChanges', () => { comp.title = 'Test Title'; fixture.detectChanges(); // detect changes explicitly expect(el.textContent).toContain(comp.title); });
The first test shows the benefit of automatic change detection.
The second and third test reveal an important limitation. The Angular testing environment does not know that the test changed the component's title
. The ComponentFixtureAutoDetect
service responds to asynchronous activities such as promise resolution, timers, and DOM events. But a direct, synchronous update of the component property is invisible. The test must call fixture.detectChanges()
manually to trigger another cycle of change detection.
Rather than wonder when the test fixture will or won't perform change detection, the samples in this guide always call
detectChanges()
explicitly. There is no harm in callingdetectChanges()
more often than is strictly necessary.
Test a component with an external template
The application's actual BannerComponent
behaves the same as the version above but is implemented differently. It has external template and css files, specified in templateUrl
and styleUrls
properties.
import { Component } from '@angular/core'; @Component({ selector: 'app-banner', templateUrl: './banner.component.html', styleUrls: ['./banner.component.css'] }) export class BannerComponent { title = 'Test Tour of Heroes'; }
That's a problem for the tests. The TestBed.createComponent
method is synchronous. But the Angular template compiler must read the external files from the file system before it can create a component instance. That's an asynchronous activity. The previous setup for testing the inline component won't work for a component with an external template.
The first asynchronous beforeEach
The test setup for BannerComponent
must give the Angular template compiler time to read the files. The logic in the beforeEach
of the previous spec is split into two beforeEach
calls. The first beforeEach
handles asynchronous compilation.
// async beforeEach beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ BannerComponent ], // declare the test component }) .compileComponents(); // compile template and css }));
Notice the async
function called as the argument to beforeEach
. The async
function is one of the Angular testing utilities and has to be imported.
import { async } from '@angular/core/testing';
It takes a parameterless function and returns a function which becomes the true argument to the beforeEach
.
The body of the async
argument looks much like the body of a synchronous beforeEach
. There is nothing obviously asynchronous about it. For example, it doesn't return a promise and there is no done
function to call as there would be in standard Jasmine asynchronous tests. Internally, async
arranges for the body of the beforeEach
to run in a special async test zone that hides the mechanics of asynchronous execution.
All this is necessary in order to call the asynchronous TestBed.compileComponents
method.
compileComponents
The TestBed.configureTestingModule
method returns the TestBed
class so you can chain calls to other TestBed
static methods such as compileComponents
.
The TestBed.compileComponents
method asynchronously compiles all the components configured in the testing module. In this example, the BannerComponent
is the only component to compile. When compileComponents
completes, the external templates and css files have been "inlined" and TestBed.createComponent
can create new instances of BannerComponent
synchronously.
WebPack developers need not call
compileComponents
because it inlines templates and css as part of the automated build process that precedes running the test.
In this example, TestBed.compileComponents
only compiles the BannerComponent
. Tests later in the guide declare multiple components and a few specs import entire application modules that hold yet more components. Any of these components might have external templates and css files. TestBed.compileComponents
compiles all of the declared components asynchronously at one time.
Do not configure the
TestBed
after callingcompileComponents
. MakecompileComponents
the last step before callingTestBed.createComponent
to instantiate the component-under-test.
Calling compileComponents
closes the current TestBed
instance to further configuration. You cannot call any more TestBed
configuration methods, not configureTestingModule
nor any of the override...
methods. The TestBed
throws an error if you try.
The second synchronous beforeEach
A synchronous beforeEach
containing the remaining setup steps follows the asynchronous beforeEach
.
// synchronous beforeEach beforeEach(() => { fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // query for the title <h1> by CSS element selector de = fixture.debugElement.query(By.css('h1')); el = de.nativeElement; });
These are the same steps as in the original beforeEach
. They include creating an instance of the BannerComponent
and querying for the elements to inspect.
You can count on the test runner to wait for the first asynchronous beforeEach
to finish before calling the second.
Waiting for compileComponents
The compileComponents
method returns a promise so you can perform additional tasks immediately after it finishes. For example, you could move the synchronous code in the second beforeEach
into a compileComponents().then(...)
callback and write only one beforeEach
.
Most developers find that hard to read. The two beforeEach
calls are widely preferred.
Try the live example
Take a moment to explore this component spec as a .
The Quickstart seed provides a similar test of its
AppComponent
as you can see in this . It too callscompileComponents
although it doesn't have to because theAppComponent
's template is inline.There's no harm in it and you might call
compileComponents
anyway in case you decide later to re-factor the template into a separate file. The tests in this guide only callcompileComponents
when necessary.
Test a component with a dependency
Components often have service dependencies.
The WelcomeComponent
displays a welcome message to the logged in user. It knows who the user is based on a property of the injected UserService
:
import { Component, OnInit } from '@angular/core'; import { UserService } from './model'; @Component({ selector: 'app-welcome', template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>' }) export class WelcomeComponent implements OnInit { welcome = '-- not initialized yet --'; constructor(private userService: UserService) { } ngOnInit(): void { this.welcome = this.userService.isLoggedIn ? 'Welcome, ' + this.userService.user.name : 'Please log in.'; } }
The WelcomeComponent
has decision logic that interacts with the service, logic that makes this component worth testing. Here's the testing module configuration for the spec file, src/app/welcome.component.spec.ts
:
TestBed.configureTestingModule({ declarations: [ WelcomeComponent ], // providers: [ UserService ] // NO! Don't provide the real service! // Provide a test-double instead providers: [ {provide: UserService, useValue: userServiceStub } ] });
This time, in addition to declaring the component-under-test, the configuration adds a UserService
provider to the providers
list. But not the real UserService
.
Provide service test doubles
A component-under-test doesn't have to be injected with real services. In fact, it is usually better if they are test doubles (stubs, fakes, spies, or mocks). The purpose of the spec is to test the component, not the service, and real services can be trouble.
Injecting the real UserService
could be a nightmare. The real service might ask the user for login credentials and attempt to reach an authentication server. These behaviors can be hard to intercept. It is far easier and safer to create and register a test double in place of the real UserService
.
This particular test suite supplies a minimal UserService
stub that satisfies the needs of the WelcomeComponent
and its tests:
userServiceStub = { isLoggedIn: true, user: { name: 'Test User'} };
Get injected services
The tests need access to the (stub) UserService
injected into the WelcomeComponent
.
Angular has a hierarchical injection system. There can be injectors at multiple levels, from the root injector created by the TestBed
down through the component tree.
The safest way to get the injected service, the way that always works, is to get it from the injector of the component-under-test. The component injector is a property of the fixture's DebugElement
.
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService);
TestBed.get
You may also be able to get the service from the root injector via TestBed.get
. This is easier to remember and less verbose. But it only works when Angular injects the component with the service instance in the test's root injector. Fortunately, in this test suite, the only provider of UserService
is the root testing module, so it is safe to call TestBed.get
as follows:
// UserService from the root injector userService = TestBed.get(UserService);
The
inject
utility function is another way to get one or more services from the test root injector.For a use case in which
inject
andTestBed.get
do not work, see the section Override a component's providers, which explains why you must get the service from the component's injector instead.
Always get the service from an injector
Do not reference the userServiceStub
object that's provided to the testing module in the body of your test. It does not work! The userService
instance injected into the component is a completely different object, a clone of the provided userServiceStub
.
it('stub object and injected UserService should not be the same', () => { expect(userServiceStub === userService).toBe(false); // Changing the stub object has no effect on the injected service userServiceStub.isLoggedIn = false; expect(userService.isLoggedIn).toBe(true); });
Final setup and tests
Here's the complete beforeEach
using TestBed.get
:
beforeEach(() => { // stub UserService for test purposes userServiceStub = { isLoggedIn: true, user: { name: 'Test User'} }; TestBed.configureTestingModule({ declarations: [ WelcomeComponent ], providers: [ {provide: UserService, useValue: userServiceStub } ] }); fixture = TestBed.createComponent(WelcomeComponent); comp = fixture.componentInstance; // UserService from the root injector userService = TestBed.get(UserService); // get the "welcome" element by CSS selector (e.g., by class name) de = fixture.debugElement.query(By.css('.welcome')); el = de.nativeElement; });
And here are some tests:
it('should welcome the user', () => { fixture.detectChanges(); const content = el.textContent; expect(content).toContain('Welcome', '"Welcome ..."'); expect(content).toContain('Test User', 'expected name'); }); it('should welcome "Bubba"', () => { userService.user.name = 'Bubba'; // welcome message hasn't been shown yet fixture.detectChanges(); expect(el.textContent).toContain('Bubba'); }); it('should request login if not logged in', () => { userService.isLoggedIn = false; // welcome message hasn't been shown yet fixture.detectChanges(); const content = el.textContent; expect(content).not.toContain('Welcome', 'not welcomed'); expect(content).toMatch(/log in/i, '"log in"'); });
The first is a sanity test; it confirms that the stubbed UserService
is called and working.
The second parameter to the Jasmine matcher (e.g.,
'expected name'
) is an optional addendum. If the expectation fails, Jasmine displays this addendum after the expectation failure message. In a spec with multiple expectations, it can help clarify what went wrong and which expectation failed.
The remaining tests confirm the logic of the component when the service returns different values. The second test validates the effect of changing the user name. The third test checks that the component displays the proper message when there is no logged-in user.
Test a component with an async service
Many services return values asynchronously. Most data services make an HTTP request to a remote server and the response is necessarily asynchronous.
The "About" view in this sample displays Mark Twain quotes. The TwainComponent
handles the display, delegating the server request to the TwainService
.
Both are in the src/app/shared
folder because the author intends to display Twain quotes on other pages someday. Here is the TwainComponent
.
@Component({ selector: 'twain-quote', template: '<p class="twain"><i>{{quote}}</i></p>' }) export class TwainComponent implements OnInit { intervalId: number; quote = '...'; constructor(private twainService: TwainService) { } ngOnInit(): void { this.twainService.getQuote().then(quote => this.quote = quote); } }
The TwainService
implementation is irrelevant for this particular test. It is sufficient to see within ngOnInit
that twainService.getQuote
returns a promise, which means it is asynchronous.
In general, tests should not make calls to remote servers. They should emulate such calls. The setup in this src/app/shared/twain.component.spec.ts
shows one way to do that:
beforeEach(() => { TestBed.configureTestingModule({ declarations: [ TwainComponent ], providers: [ TwainService ], }); fixture = TestBed.createComponent(TwainComponent); comp = fixture.componentInstance; // TwainService actually injected into the component twainService = fixture.debugElement.injector.get(TwainService); // Setup spy on the `getQuote` method spy = spyOn(twainService, 'getQuote') .and.returnValue(Promise.resolve(testQuote)); // Get the Twain quote element by CSS selector (e.g., by class name) de = fixture.debugElement.query(By.css('.twain')); el = de.nativeElement; });
Spying on the real service
This setup is similar to the welcome.component.spec
setup. But instead of creating a stubbed service object, it injects the real service (see the testing module providers
) and replaces the critical getQuote
method with a Jasmine spy.
spy = spyOn(twainService, 'getQuote') .and.returnValue(Promise.resolve(testQuote));
The spy is designed such that any call to getQuote
receives an immediately resolved promise with a test quote. The spy bypasses the actual getQuote
method and therefore does not contact the server.
Faking a service instance and spying on the real service are both great options. Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind.
Spying on the real service isn't always easy, especially when the real service has injected dependencies. You can stub and spy at the same time, as shown in an example below.
Here are the tests with commentary to follow:
it('should not show quote before OnInit', () => { expect(el.textContent).toBe('', 'nothing displayed'); expect(spy.calls.any()).toBe(false, 'getQuote not yet called'); }); it('should still not show quote after component initialized', () => { fixture.detectChanges(); // getQuote service is async => still has not returned with quote expect(el.textContent).toBe('...', 'no quote yet'); expect(spy.calls.any()).toBe(true, 'getQuote called'); }); it('should show quote after getQuote promise (async)', async(() => { fixture.detectChanges(); fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }); })); it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { fixture.detectChanges(); tick(); // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }));
Synchronous tests
The first two tests are synchronous. Thanks to the spy, they verify that getQuote
is called after the first change detection cycle during which Angular calls ngOnInit
.
Neither test can prove that a value from the service is displayed. The quote itself has not arrived, despite the fact that the spy returns a resolved promise.
This test must wait at least one full turn of the JavaScript engine before the value becomes available. The test must become asynchronous.
The async
function in it
Notice the async
in the third test.
it('should show quote after getQuote promise (async)', async(() => { fixture.detectChanges(); fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }); }));
The async
function is one of the Angular testing utilities. It simplifies coding of asynchronous tests by arranging for the tester's code to run in a special async test zone as discussed earlier when it was called in a beforeEach
.
Although async
does a great job of hiding asynchronous boilerplate, some functions called within a test (such as fixture.whenStable
) continue to reveal their asynchronous behavior.
The
fakeAsync
alternative, covered below, removes this artifact and affords a more linear coding experience.
whenStable
The test must wait for the getQuote
promise to resolve in the next turn of the JavaScript engine.
This test has no direct access to the promise returned by the call to twainService.getQuote
because it is buried inside TwainComponent.ngOnInit
and therefore inaccessible to a test that probes only the component API surface.
Fortunately, the getQuote
promise is accessible to the async test zone, which intercepts all promises issued within the async method call no matter where they occur.
The ComponentFixture.whenStable
method returns its own promise, which resolves when the getQuote
promise finishes. In fact, the whenStable promise resolves when all pending asynchronous activities within this test complete—the definition of "stable."
Then the test resumes and kicks off another round of change detection (fixture.detectChanges
), which tells Angular to update the DOM with the quote. The getQuote
helper method extracts the display element text and the expectation confirms that the text matches the test quote.
The fakeAsync
function
The fourth test verifies the same component behavior in a different way.
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { fixture.detectChanges(); tick(); // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }));
Notice that fakeAsync
replaces async
as the it
argument. The fakeAsync
function is another of the Angular testing utilities.
Like async, it takes a parameterless function and returns a function that becomes the argument to the Jasmine it
call.
The fakeAsync
function enables a linear coding style by running the test body in a special fakeAsync test zone.
The principle advantage of fakeAsync
over async
is that the test appears to be synchronous. There is no then(...)
to disrupt the visible flow of control. The promise-returning fixture.whenStable
is gone, replaced by tick()
.
There are limitations. For example, you cannot make an XHR call from within a
fakeAsync
.
The tick
function
The tick
function is one of the Angular testing utilities and a companion to fakeAsync
. You can only call it within a fakeAsync
body.
Calling tick()
simulates the passage of time until all pending asynchronous activities finish, including the resolution of the getQuote
promise in this test case.
It returns nothing. There is no promise to wait for. Proceed with the same test code that appeared in the whenStable.then()
callback.
Even this simple example is easier to read than the third test. To more fully appreciate the improvement, imagine a succession of asynchronous operations, chained in a long sequence of promise callbacks.
jasmine.done
While the async
and fakeAsync
functions greatly simplify Angular asynchronous testing, you can still fall back to the traditional Jasmine asynchronous testing technique.
You can still pass it
a function that takes a done
callback. Now you are responsible for chaining promises, handling errors, and calling done
at the appropriate moment.
Here is a done
version of the previous two tests:
it('should show quote after getQuote promise (done)', (done: any) => { fixture.detectChanges(); // get the spy promise and wait for it to resolve spy.calls.mostRecent().returnValue.then(() => { fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); done(); }); });
Although there is no direct access to the getQuote
promise inside TwainComponent
, the spy has direct access, which makes it possible to wait for getQuote
to finish.
Writing test functions with done
, while more cumbersome than async
and fakeAsync
, is a viable and occasionally necessary technique. For example, you can't call async
or fakeAsync
when testing code that involves the intervalTimer
, as is common when testing async Observable
methods.
Test a component with inputs and outputs
A component with inputs and outputs typically appears inside the view template of a host component. The host uses a property binding to set the input property and an event binding to listen to events raised by the output property.
The testing goal is to verify that such bindings work as expected. The tests should set input values and listen for output events.
The DashboardHeroComponent
is a tiny example of a component in this role. It displays an individual hero provided by the DashboardComponent
. Clicking that hero tells the DashboardComponent
that the user has selected the hero.
The DashboardHeroComponent
is embedded in the DashboardComponent
template like this:
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4" [hero]=hero (selected)="gotoDetail($event)" > </dashboard-hero>
The DashboardHeroComponent
appears in an *ngFor
repeater, which sets each component's hero
input property to the looping value and listens for the component's selected
event.
Here's the component's definition:
@Component({ selector: 'dashboard-hero', templateUrl: './dashboard-hero.component.html', styleUrls: [ './dashboard-hero.component.css' ] }) export class DashboardHeroComponent { @Input() hero: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.emit(this.hero); } }
While testing a component this simple has little intrinsic value, it's worth knowing how. You can use one of these approaches:
- Test it as used by
DashboardComponent
. - Test it as a stand-alone component.
- Test it as used by a substitute for
DashboardComponent
.
A quick look at the DashboardComponent
constructor discourages the first approach:
constructor( private router: Router, private heroService: HeroService) { }
The DashboardComponent
depends on the Angular router and the HeroService
. You'd probably have to replace them both with test doubles, which is a lot of work. The router seems particularly challenging.
The discussion below covers testing components that require the router.
The immediate goal is to test the DashboardHeroComponent
, not the DashboardComponent
, so, try the second and third options.
Test DashboardHeroComponent
stand-alone
Here's the spec file setup.
// async beforeEach beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ DashboardHeroComponent ], }) .compileComponents(); // compile template and css })); // synchronous beforeEach beforeEach(() => { fixture = TestBed.createComponent(DashboardHeroComponent); comp = fixture.componentInstance; heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element // pretend that it was wired to something that supplied a hero expectedHero = new Hero(42, 'Test Name'); comp.hero = expectedHero; fixture.detectChanges(); // trigger initial data binding });
The async beforeEach
was discussed above. Having compiled the components asynchronously with compileComponents
, the rest of the setup proceeds synchronously in a second beforeEach
, using the basic techniques described earlier.
Note how the setup code assigns a test hero (expectedHero
) to the component's hero
property, emulating the way the DashboardComponent
would set it via the property binding in its repeater.
The first test follows:
it('should display hero name', () => { const expectedPipedName = expectedHero.name.toUpperCase(); expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); });
It verifies that the hero name is propagated to template with a binding. Because the template passes the hero name through the Angular UpperCasePipe
, the test must match the element value with the uppercased name:
<div (click)="click()" class="hero"> {{hero.name | uppercase}} </div>
This small test demonstrates how Angular tests can verify a component's visual representation—something not possible with isolated unit tests—at low cost and without resorting to much slower and more complicated end-to-end tests.
The second test verifies click behavior. Clicking the hero should raise a selected
event that the host component (DashboardComponent
presumably) can hear:
it('should raise selected event when clicked', () => { let selectedHero: Hero; comp.selected.subscribe((hero: Hero) => selectedHero = hero); heroEl.triggerEventHandler('click', null); expect(selectedHero).toBe(expectedHero); });
The component exposes an EventEmitter
property. The test subscribes to it just as the host component would do.
The heroEl
is a DebugElement
that represents the hero <div>
. The test calls triggerEventHandler
with the "click" event name. The "click" event binding responds by calling DashboardHeroComponent.click()
.
If the component behaves as expected, click()
tells the component's selected
property to emit the hero
object, the test detects that value through its subscription to selected
, and the test should pass.
triggerEventHandler
The Angular DebugElement.triggerEventHandler
can raise any data-bound event by its event name. The second parameter is the event object passed to the handler.
In this example, the test triggers a "click" event with a null event object.
heroEl.triggerEventHandler('click', null);
The test assumes (correctly in this case) that the runtime event handler—the component's click()
method—doesn't care about the event object.
Other handlers are less forgiving. For example, the RouterLink
directive expects an object with a button
property that identifies which mouse button was pressed. This directive throws an error if the event object doesn't do this correctly.
Clicking a button, an anchor, or an arbitrary HTML element is a common test task.
Make that easy by encapsulating the click-triggering process in a helper such as the click
function below:
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ export const ButtonClickEvents = { left: { button: 0 }, right: { button: 2 } }; /** Simulate element click. Defaults to mouse left-button click event. */ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void { if (el instanceof HTMLElement) { el.click(); } else { el.triggerEventHandler('click', eventObj); } }
The first parameter is the element-to-click. If you wish, you can pass a custom event object as the second parameter. The default is a (partial) left-button mouse event object accepted by many handlers including the RouterLink
directive.
The click()
helper function is not one of the Angular testing utilities. It's a function defined in this guide's sample code. All of the sample tests use it. If you like it, add it to your own collection of helpers.
Here's the previous test, rewritten using this click helper.
it('should raise selected event when clicked', () => { let selectedHero: Hero; comp.selected.subscribe((hero: Hero) => selectedHero = hero); click(heroEl); // triggerEventHandler helper expect(selectedHero).toBe(expectedHero); });
Test a component inside a test host component
In the previous approach, the tests themselves played the role of the host DashboardComponent
. But does the DashboardHeroComponent
work correctly when properly data-bound to a host component?
Testing with the actual DashboardComponent
host is doable but seems more trouble than its worth. It's easier to emulate the DashboardComponent
host with a test host like this one:
@Component({ template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>` }) class TestHostComponent { hero = new Hero(42, 'Test Name'); selectedHero: Hero; onSelected(hero: Hero) { this.selectedHero = hero; } }
The test host binds to DashboardHeroComponent
as the DashboardComponent
would but without the distraction of the Router
, the HeroService
, or even the *ngFor
repeater.
The test host sets the component's hero
input property with its test hero. It binds the component's selected
event with its onSelected
handler, which records the emitted hero in its selectedHero
property. Later, the tests check that property to verify that the DashboardHeroComponent.selected
event emitted the right hero.
The setup for the test-host tests is similar to the setup for the stand-alone tests:
beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both }).compileComponents(); })); beforeEach(() => { // create TestHostComponent instead of DashboardHeroComponent fixture = TestBed.createComponent(TestHostComponent); testHost = fixture.componentInstance; heroEl = fixture.debugElement.query(By.css('.hero')); // find hero fixture.detectChanges(); // trigger initial data binding });
This testing module configuration shows two important differences:
- It declares both the
DashboardHeroComponent
and theTestHostComponent
. - It creates the
TestHostComponent
instead of theDashboardHeroComponent
.
The createComponent
returns a fixture
that holds an instance of TestHostComponent
instead of an instance of DashboardHeroComponent
.
Creating the TestHostComponent
has the side-effect of creating a DashboardHeroComponent
because the latter appears within the template of the former. The query for the hero element (heroEl
) still finds it in the test DOM, albeit at greater depth in the element tree than before.
The tests themselves are almost identical to the stand-alone version:
it('should display hero name', () => { const expectedPipedName = testHost.hero.name.toUpperCase(); expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); }); it('should raise selected event when clicked', () => { click(heroEl); // selected hero should be the same data bound hero expect(testHost.selectedHero).toBe(testHost.hero); });
Only the selected event test differs. It confirms that the selected DashboardHeroComponent
hero really does find its way up through the event binding to the host component.
Test a routed component
Testing the actual DashboardComponent
seemed daunting because it injects the Router
.
constructor( private router: Router, private heroService: HeroService) { }
It also injects the HeroService
, but faking that is a familiar story. The Router
has a complicated API and is entwined with other services and application preconditions.
Fortunately, the DashboardComponent
isn't doing much with the Router
gotoDetail(hero: Hero) { let url = `/heroes/${hero.id}`; this.router.navigateByUrl(url); }
This is often the case. As a rule you test the component, not the router, and care only if the component navigates with the right address under the given conditions. Stubbing the router with a test implementation is an easy option. This should do the trick:
class RouterStub { navigateByUrl(url: string) { return url; } }
Now set up the testing module with the test stubs for the Router
and HeroService
, and create a test instance of the DashboardComponent
for subsequent testing.
beforeEach( async(() => { TestBed.configureTestingModule({ providers: [ { provide: HeroService, useClass: FakeHeroService }, { provide: Router, useClass: RouterStub } ] }) .compileComponents().then(() => { fixture = TestBed.createComponent(DashboardComponent); comp = fixture.componentInstance; });
The following test clicks the displayed hero and confirms (with the help of a spy) that Router.navigateByUrl
is called with the expected url.
it('should tell ROUTER to navigate when hero clicked', inject([Router], (router: Router) => { // ... const spy = spyOn(router, 'navigateByUrl'); heroClick(); // trigger click on first inner <div class="hero"> // args passed to router.navigateByUrl() const navArgs = spy.calls.first().args[0]; // expecting to navigate to id of the component's first hero const id = comp.heroes[0].id; expect(navArgs).toBe('/heroes/' + id, 'should nav to HeroDetail for first hero'); }));
The inject
function
Notice the inject
function in the second it
argument.
it('should tell ROUTER to navigate when hero clicked', inject([Router], (router: Router) => { // ... }));
The inject
function is one of the Angular testing utilities. It injects services into the test function where you can alter, spy on, and manipulate them.
The inject
function has two parameters:
- An array of Angular dependency injection tokens.
- A test function whose parameters correspond exactly to each item in the injection token array.
The inject
function uses the current TestBed
injector and can only return services provided at that level. It does not return services from component providers.
This example injects the Router
from the current TestBed
injector. That's fine for this test because the Router
is, and must be, provided by the application root injector.
If you need a service provided by the component's own injector, call fixture.debugElement.injector.get
instead:
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService);
Use the component's own injector to get the service actually injected into the component.
The inject
function closes the current TestBed
instance to further configuration. You cannot call any more TestBed
configuration methods, not configureTestingModule
nor any of the override...
methods. The TestBed
throws an error if you try.
Test a routed component with parameters
Clicking a Dashboard hero triggers navigation to heroes/:id
, where :id
is a route parameter whose value is the id
of the hero to edit. That URL matches a route to the HeroDetailComponent
.
The router pushes the :id
token value into the ActivatedRoute.params
Observable property, Angular injects the ActivatedRoute
into the HeroDetailComponent
, and the component extracts the id
so it can fetch the corresponding hero via the HeroDetailService
. Here's the HeroDetailComponent
constructor:
constructor( private heroDetailService: HeroDetailService, private route: ActivatedRoute, private router: Router) { }
HeroDetailComponent
subscribes to ActivatedRoute.params
changes in its ngOnInit
method.
ngOnInit(): void { // get hero when `id` param changes this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id'))); }
The expression after
route.params
chains an Observable operator that plucks theid
from theparams
and then chains aforEach
operator to subscribe toid
-changing events. Theid
changes every time the user navigates to a different hero.The
forEach
passes the newid
value to the component'sgetHero
method (not shown) which fetches a hero and sets the component'shero
property. If theid
parameter is missing, thepluck
operator fails and thecatch
treats failure as a request to edit a new hero.The Router guide covers
ActivatedRoute.params
in more detail.
A test can explore how the HeroDetailComponent
responds to different id
parameter values by manipulating the ActivatedRoute
injected into the component's constructor.
By now you know how to stub the Router
and a data service. Stubbing the ActivatedRoute
follows the same pattern except for a complication: the ActivatedRoute.params
is an Observable.
Create an Observable
test double
The hero-detail.component.spec.ts
relies on an ActivatedRouteStub
to set ActivatedRoute.params
values for each test. This is a cross-application, re-usable test helper class. Consider placing such helpers in a testing
folder sibling to the app
folder. This sample keeps ActivatedRouteStub
in testing/router-stubs.ts
:
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { convertToParamMap, ParamMap } from '@angular/router'; @Injectable() export class ActivatedRouteStub { // ActivatedRoute.paramMap is Observable private subject = new BehaviorSubject(convertToParamMap(this.testParamMap)); paramMap = this.subject.asObservable(); // Test parameters private _testParamMap: ParamMap; get testParamMap() { return this._testParamMap; } set testParamMap(params: {}) { this._testParamMap = convertToParamMap(params); this.subject.next(this._testParamMap); } // ActivatedRoute.snapshot.paramMap get snapshot() { return { paramMap: this.testParamMap }; } }
Notable features of this stub are:
-
The stub implements only two of the
ActivatedRoute
capabilities:params
andsnapshot.params
. -
BehaviorSubject drives the stub's
params
Observable and returns the same value to everyparams
subscriber until it's given a new value. -
The
HeroDetailComponent
chains its expressions to this stubparams
Observable which is now under the tester's control. -
Setting the
testParams
property causes thesubject
to push the assigned value intoparams
. That triggers theHeroDetailComponent
params subscription, described above, in the same way that navigation does. -
Setting the
testParams
property also updates the stub's internal value for thesnapshot
property to return.
The snapshot is another popular way for components to consume route parameters.
The router stubs in this guide are meant to inspire you. Create your own stubs to fit your testing needs.
Testing with the Observable
test double
Here's a test demonstrating the component's behavior when the observed id
refers to an existing hero:
describe('when navigate to existing hero', () => { let expectedHero: Hero; beforeEach( async(() => { expectedHero = firstHero; activatedRoute.testParamMap = { id: expectedHero.id }; createComponent(); })); it('should display that hero\'s name', () => { expect(page.nameDisplay.textContent).toBe(expectedHero.name); }); });
The
createComponent
method andpage
object are discussed in the next section. Rely on your intuition for now.
When the id
cannot be found, the component should re-route to the HeroListComponent
. The test suite setup provided the same RouterStub
described above which spies on the router without actually navigating. This test supplies a "bad" id and expects the component to try to navigate.
describe('when navigate to non-existent hero id', () => { beforeEach( async(() => { activatedRoute.testParamMap = { id: 99999 }; createComponent(); })); it('should try to navigate back to hero list', () => { expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called'); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); }); });
While this app doesn't have a route to the HeroDetailComponent
that omits the id
parameter, it might add such a route someday. The component should do something reasonable when there is no id
.
In this implementation, the component should create and display a new hero. New heroes have id=0
and a blank name
. This test confirms that the component behaves as expected:
describe('when navigate with no hero id', () => { beforeEach( async( createComponent )); it('should have hero.id === 0', () => { expect(comp.hero.id).toBe(0); }); it('should display empty hero name', () => { expect(page.nameDisplay.textContent).toBe(''); }); });
Inspect and download all of the guide's application test code with this live example.
Use a page
object to simplify setup
The HeroDetailComponent
is a simple view with a title, two hero fields, and two buttons.
But there's already plenty of template complexity.
<div *ngIf="hero"> <h2><span>{{hero.name | titlecase}}</span> Details</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label for="name">name: </label> <input id="name" [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="save()">Save</button> <button (click)="cancel()">Cancel</button> </div>
To fully exercise the component, the test needs a lot of setup:
- It must wait until a hero arrives before
*ngIf
allows any element in DOM. - It needs references to the title
<span>
and the name<input>
so it can inspect their values. - It needs references to the two buttons so it can click them.
- It needs spies for some of the component and router methods.
Even a small form such as this one can produce a mess of tortured conditional setup and CSS element selection.
Tame the madness with a Page
class that simplifies access to component properties and encapsulates the logic that sets them. Here's the Page
class for the hero-detail.component.spec.ts
class Page { gotoSpy: jasmine.Spy; navSpy: jasmine.Spy; saveBtn: DebugElement; cancelBtn: DebugElement; nameDisplay: HTMLElement; nameInput: HTMLInputElement; constructor() { const router = TestBed.get(Router); // get router from root injector this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); this.navSpy = spyOn(router, 'navigate'); } /** Add page elements after hero arrives */ addPageElements() { if (comp.hero) { // have a hero so these elements are now in the DOM const buttons = fixture.debugElement.queryAll(By.css('button')); this.saveBtn = buttons[0]; this.cancelBtn = buttons[1]; this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement; this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement; } } }
Now the important hooks for component manipulation and inspection are neatly organized and accessible from an instance of Page
.
A createComponent
method creates a page
object and fills in the blanks once the hero
arrives.
/** Create the HeroDetailComponent, initialize it, set test variables */ function createComponent() { fixture = TestBed.createComponent(HeroDetailComponent); comp = fixture.componentInstance; page = new Page(); // 1st change detection triggers ngOnInit which gets a hero fixture.detectChanges(); return fixture.whenStable().then(() => { // 2nd change detection displays the async-fetched hero fixture.detectChanges(); page.addPageElements(); }); }
The observable tests in the previous section demonstrate how createComponent
and page
keep the tests short and on message. There are no distractions: no waiting for promises to resolve and no searching the DOM for element values to compare.
Here are a few more HeroDetailComponent
tests to drive the point home.
it('should display that hero\'s name', () => { expect(page.nameDisplay.textContent).toBe(expectedHero.name); }); it('should navigate when click cancel', () => { click(page.cancelBtn); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); }); it('should save when click save but not navigate immediately', () => { // Get service injected into component and spy on its`saveHero` method. // It delegates to fake `HeroService.updateHero` which delivers a safe test result. const hds = fixture.debugElement.injector.get(HeroDetailService); const saveSpy = spyOn(hds, 'saveHero').and.callThrough(); click(page.saveBtn); expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called'); }); it('should navigate when click save and save resolves', fakeAsync(() => { click(page.saveBtn); tick(); // wait for async save to complete expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); })); it('should convert hero name to Title Case', () => { const inputName = 'quick BROWN fox'; const titleCaseName = 'Quick Brown Fox'; // simulate user entering new name into the input box page.nameInput.value = inputName; // dispatch a DOM event so that Angular learns of input value change. page.nameInput.dispatchEvent(newEvent('input')); // Tell Angular to update the output span through the title pipe fixture.detectChanges(); expect(page.nameDisplay.textContent).toBe(titleCaseName); });
Setup with module imports
Earlier component tests configured the testing module with a few declarations
like this:
// async beforeEach beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ DashboardHeroComponent ], }) .compileComponents(); // compile template and css }));
The DashboardComponent
is simple. It needs no help. But more complex components often depend on other components, directives, pipes, and providers and these must be added to the testing module too.
Fortunately, the TestBed.configureTestingModule
parameter parallels the metadata passed to the @NgModule
decorator which means you can also specify providers
and imports
.
The HeroDetailComponent
requires a lot of help despite its small size and simple construction. In addition to the support it receives from the default testing module CommonModule
, it needs:
-
NgModel
and friends in theFormsModule
to enable two-way data binding. - The
TitleCasePipe
from theshared
folder. - Router services (which these tests are stubbing).
- Hero data access services (also stubbed).
One approach is to configure the testing module from the individual pieces as in this example:
beforeEach( async(() => { TestBed.configureTestingModule({ imports: [ FormsModule ], declarations: [ HeroDetailComponent, TitleCasePipe ], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HeroService, useClass: FakeHeroService }, { provide: Router, useClass: RouterStub}, ] }) .compileComponents(); }));
Because many app components need the FormsModule
and the TitleCasePipe
, the developer created a SharedModule
to combine these and other frequently requested parts. The test configuration can use the SharedModule
too as seen in this alternative setup:
beforeEach( async(() => { TestBed.configureTestingModule({ imports: [ SharedModule ], declarations: [ HeroDetailComponent ], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HeroService, useClass: FakeHeroService }, { provide: Router, useClass: RouterStub}, ] }) .compileComponents(); }));
It's a bit tighter and smaller, with fewer import statements (not shown).
Import the feature module
The HeroDetailComponent
is part of the HeroModule
Feature Module that aggregates more of the interdependent pieces including the SharedModule
. Try a test configuration that imports the HeroModule
like this one:
beforeEach( async(() => { TestBed.configureTestingModule({ imports: [ HeroModule ], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HeroService, useClass: FakeHeroService }, { provide: Router, useClass: RouterStub}, ] }) .compileComponents(); }));
That's really crisp. Only the test doubles in the providers
remain. Even the HeroDetailComponent
declaration is gone.
In fact, if you try to declare it, Angular throws an error because
HeroDetailComponent
is declared in both theHeroModule
and theDynamicTestModule
(the testing module).
Importing the component's feature module is often the easiest way to configure the tests, especially when the feature module is small and mostly self-contained, as feature modules should be.
Override a component's providers
The HeroDetailComponent
provides its own HeroDetailService
.
@Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css' ], providers: [ HeroDetailService ] }) export class HeroDetailComponent implements OnInit { constructor( private heroDetailService: HeroDetailService, private route: ActivatedRoute, private router: Router) { } }
It's not possible to stub the component's HeroDetailService
in the providers
of the TestBed.configureTestingModule
. Those are providers for the testing module, not the component. They prepare the dependency injector at the fixture level.
Angular creates the component with its own injector, which is a child of the fixture injector. It registers the component's providers (the HeroDetailService
in this case) with the child injector. A test cannot get to child injector services from the fixture injector. And TestBed.configureTestingModule
can't configure them either.
Angular has been creating new instances of the real HeroDetailService
all along!
These tests could fail or timeout if the
HeroDetailService
made its own XHR calls to a remote server. There might not be a remote server to call.Fortunately, the
HeroDetailService
delegates responsibility for remote data access to an injectedHeroService
.src/app/hero/hero-detail.service.ts (prototype)@Injectable() export class HeroDetailService { constructor(private heroService: HeroService) { } /* . . . */ }The previous test configuration replaces the real
HeroService
with aFakeHeroService
that intercepts server requests and fakes their responses.
What if you aren't so lucky. What if faking the HeroService
is hard? What if HeroDetailService
makes its own server requests?
The TestBed.overrideComponent
method can replace the component's providers
with easy-to-manage test doubles as seen in the following setup variation:
beforeEach( async(() => { TestBed.configureTestingModule({ imports: [ HeroModule ], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, { provide: Router, useClass: RouterStub}, ] }) // Override component's own provider .overrideComponent(HeroDetailComponent, { set: { providers: [ { provide: HeroDetailService, useClass: HeroDetailServiceSpy } ] } }) .compileComponents(); }));
Notice that TestBed.configureTestingModule
no longer provides a (fake) HeroService
because it's not needed.
The overrideComponent
method
Focus on the overrideComponent
method.
.overrideComponent(HeroDetailComponent, { set: { providers: [ { provide: HeroDetailService, useClass: HeroDetailServiceSpy } ] } })
It takes two arguments: the component type to override (HeroDetailComponent
) and an override metadata object. The overide metadata object is a generic defined as follows:
type MetadataOverride = { add?: T; remove?: T; set?: T; };
A metadata override object can either add-and-remove elements in metadata properties or completely reset those properties. This example resets the component's providers
metadata.
The type parameter, T
, is the kind of metadata you'd pass to the @Component
decorator:
selector?: string; template?: string; templateUrl?: string; providers?: any[]; ...
Provide a spy stub
(HeroDetailServiceSpy
)
This example completely replaces the component's providers
array with a new array containing a HeroDetailServiceSpy
.
The HeroDetailServiceSpy
is a stubbed version of the real HeroDetailService
that fakes all necessary features of that service. It neither injects nor delegates to the lower level HeroService
so there's no need to provide a test double for that.
The related HeroDetailComponent
tests will assert that methods of the HeroDetailService
were called by spying on the service methods. Accordingly, the stub implements its methods as spies:
class HeroDetailServiceSpy { testHero = new Hero(42, 'Test Hero'); getHero = jasmine.createSpy('getHero').and.callFake( () => Promise .resolve(true) .then(() => Object.assign({}, this.testHero)) ); saveHero = jasmine.createSpy('saveHero').and.callFake( (hero: Hero) => Promise .resolve(true) .then(() => Object.assign(this.testHero, hero)) ); }
The override tests
Now the tests can control the component's hero directly by manipulating the spy-stub's testHero
and confirm that service methods were called.
let hdsSpy: HeroDetailServiceSpy; beforeEach( async(() => { createComponent(); // get the component's injected HeroDetailServiceSpy hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any; })); it('should have called `getHero`', () => { expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once'); }); it('should display stub hero\'s name', () => { expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); }); it('should save stub hero change', fakeAsync(() => { const origName = hdsSpy.testHero.name; const newName = 'New Name'; page.nameInput.value = newName; page.nameInput.dispatchEvent(newEvent('input')); // tell Angular expect(comp.hero.name).toBe(newName, 'component hero has new name'); expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save'); click(page.saveBtn); expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once'); tick(); // wait for async save to complete expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save'); expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); }));
More overrides
The TestBed.overrideComponent
method can be called multiple times for the same or different components. The TestBed
offers similar overrideDirective
, overrideModule
, and overridePipe
methods for digging into and replacing parts of these other classes.
Explore the options and combinations on your own.
Test a RouterOutlet
component
The AppComponent
displays routed components in a <router-outlet>
. It also displays a navigation bar with anchors and their RouterLink
directives.
<app-banner></app-banner> <app-welcome></app-welcome> <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/heroes">Heroes</a> <a routerLink="/about">About</a> </nav> <router-outlet></router-outlet>
The component class does nothing.
import { Component } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.component.html' }) export class AppComponent { }
Unit tests can confirm that the anchors are wired properly without engaging the router. See why this is worth doing below.
Stubbing unneeded components
The test setup should look familiar.
beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, BannerComponent, WelcomeStubComponent, RouterLinkStubDirective, RouterOutletStubComponent ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); }));
The AppComponent
is the declared test subject.
The setup extends the default testing module with one real component (BannerComponent
) and several stubs.
-
BannerComponent
is simple and harmless to use as is. -
The real
WelcomeComponent
has an injected service.WelcomeStubComponent
is a placeholder with no service to worry about. -
The real
RouterOutlet
is complex and errors easily. TheRouterOutletStubComponent
(intesting/router-stubs.ts
) is safely inert.
The component stubs are essential. Without them, the Angular compiler doesn't recognize the <app-welcome>
and <router-outlet>
tags and throws an error.
Stubbing the RouterLink
The RouterLinkStubDirective
contributes substantively to the test:
@Directive({ selector: '[routerLink]', host: { '(click)': 'onClick()' } }) export class RouterLinkStubDirective { @Input('routerLink') linkParams: any; navigatedTo: any = null; onClick() { this.navigatedTo = this.linkParams; } }
The host
metadata property wires the click event of the host element (the <a>
) to the directive's onClick
method. The URL bound to the [routerLink]
attribute flows to the directive's linkParams
property. Clicking the anchor should trigger the onClick
method which sets the telltale navigatedTo
property. Tests can inspect that property to confirm the expected click-to-navigation behavior.
By.directive
and injected directives
A little more setup triggers the initial data binding and gets references to the navigation links:
beforeEach(() => { // trigger initial data binding fixture.detectChanges(); // find DebugElements with an attached RouterLinkStubDirective linkDes = fixture.debugElement .queryAll(By.directive(RouterLinkStubDirective)); // get the attached link directive instances using the DebugElement injectors links = linkDes .map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective); });
Two points of special interest:
-
You can locate elements by directive, using
By.directive
, not just by css selectors. -
You can use the component's dependency injector to get an attached directive because Angular always adds attached directives to the component's injector.
Here are some tests that leverage this setup:
it('can get RouterLinks from template', () => { expect(links.length).toBe(3, 'should have 3 links'); expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard'); expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes'); }); it('can click Heroes link in template', () => { const heroesLinkDe = linkDes[1]; const heroesLink = links[1]; expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet'); heroesLinkDe.triggerEventHandler('click', null); fixture.detectChanges(); expect(heroesLink.navigatedTo).toBe('/heroes'); });
The "click" test in this example is worthless. It works hard to appear useful when in fact it tests the
RouterLinkStubDirective
rather than the component. This is a common failing of directive stubs.It has a legitimate purpose in this guide. It demonstrates how to find a
RouterLink
element, click it, and inspect a result, without engaging the full router machinery. This is a skill you may need to test a more sophisticated component, one that changes the display, re-calculates parameters, or re-arranges navigation options when the user clicks the link.
What good are these tests?
Stubbed RouterLink
tests can confirm that a component with links and an outlet is setup properly, that the component has the links it should have, and that they are all pointing in the expected direction. These tests do not concern whether the app will succeed in navigating to the target component when the user clicks a link.
Stubbing the RouterLink and RouterOutlet is the best option for such limited testing goals. Relying on the real router would make them brittle. They could fail for reasons unrelated to the component. For example, a navigation guard could prevent an unauthorized user from visiting the HeroListComponent
. That's not the fault of the AppComponent
and no change to that component could cure the failed test.
A different battery of tests can explore whether the application navigates as expected in the presence of conditions that influence guards such as whether the user is authenticated and authorized.
A future guide update will explain how to write such tests with the
RouterTestingModule
.
"Shallow component tests" with NO_ERRORS_SCHEMA
The previous setup declared the BannerComponent
and stubbed two other components for no reason other than to avoid a compiler error.
Without them, the Angular compiler doesn't recognize the <app-banner>
, <app-welcome>
and <router-outlet>
tags in the app.component.html template and throws an error.
Add NO_ERRORS_SCHEMA
to the testing module's schemas
metadata to tell the compiler to ignore unrecognized elements and attributes. You no longer have to declare irrelevant components and directives.
These tests are shallow because they only "go deep" into the components you want to test.
Here is a setup, with import
statements, that demonstrates the improved simplicity of shallow tests, relative to the stubbing setup.
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { AppComponent } from './app.component'; import { RouterOutletStubComponent } from '../testing'; beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, RouterLinkStubDirective ], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); }));
import { Component } from '@angular/core'; import { AppComponent } from './app.component'; import { BannerComponent } from './banner.component'; import { RouterLinkStubDirective } from '../testing'; import { RouterOutletStubComponent } from '../testing'; @Component({selector: 'app-welcome', template: ''}) class WelcomeStubComponent {} beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, BannerComponent, WelcomeStubComponent, RouterLinkStubDirective, RouterOutletStubComponent ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); }));
The only declarations are the component-under-test (AppComponent
) and the RouterLinkStubDirective
that contributes actively to the tests. The tests in this example are unchanged.
Shallow component tests with
NO_ERRORS_SCHEMA
greatly simplify unit testing of complex templates. However, the compiler no longer alerts you to mistakes such as misspelled or misused components and directives.
Test an attribute directive
An attribute directive modifies the behavior of an element, component or another directive. Its name reflects the way the directive is applied: as an attribute on a host element.
The sample application's HighlightDirective
sets the background color of an element based on either a data bound color or a default color (lightgray). It also sets a custom property of the element (customProperty
) to true
for no reason other than to show that it can.
import { Directive, ElementRef, Input, OnChanges } from '@angular/core'; @Directive({ selector: '[highlight]' }) /** Set backgroundColor for the attached element to highlight color * and set the element's customProperty to true */ export class HighlightDirective implements OnChanges { defaultColor = 'rgb(211, 211, 211)'; // lightgray @Input('highlight') bgColor: string; constructor(private el: ElementRef) { el.nativeElement.style.customProperty = true; } ngOnChanges() { this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor; } }
It's used throughout the application, perhaps most simply in the AboutComponent
:
import { Component } from '@angular/core'; @Component({ template: ` <h2 highlight="skyblue">About</h2> <twain-quote></twain-quote> <p>All about this sample</p>` }) export class AboutComponent { }
Testing the specific use of the HighlightDirective
within the AboutComponent
requires only the techniques explored above (in particular the "Shallow test" approach).
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ AboutComponent, HighlightDirective], schemas: [ NO_ERRORS_SCHEMA ] }) .createComponent(AboutComponent); fixture.detectChanges(); // initial binding }); it('should have skyblue <h2>', () => { const de = fixture.debugElement.query(By.css('h2')); const bgColor = de.nativeElement.style.backgroundColor; expect(bgColor).toBe('skyblue'); });
However, testing a single use case is unlikely to explore the full range of a directive's capabilities. Finding and testing all components that use the directive is tedious, brittle, and almost as unlikely to afford full coverage.
Isolated unit tests might be helpful, but attribute directives like this one tend to manipulate the DOM. Isolated unit tests don't touch the DOM and, therefore, do not inspire confidence in the directive's efficacy.
A better solution is to create an artificial test component that demonstrates all ways to apply the directive.
@Component({ template: ` <h2 highlight="yellow">Something Yellow</h2> <h2 highlight>The Default (Gray)</h2> <h2>No Highlight</h2> <input #box [highlight]="box.value" value="cyan"/>` }) class TestComponent { }
The
<input>
case binds theHighlightDirective
to the name of a color value in the input box. The initial value is the word "cyan" which should be the background color of the input box.
Here are some tests of this component:
beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ HighlightDirective, TestComponent ] }) .createComponent(TestComponent); fixture.detectChanges(); // initial binding // all elements with an attached HighlightDirective des = fixture.debugElement.queryAll(By.directive(HighlightDirective)); // the h2 without the HighlightDirective bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])')); }); // color tests it('should have three highlighted elements', () => { expect(des.length).toBe(3); }); it('should color 1st <h2> background "yellow"', () => { const bgColor = des[0].nativeElement.style.backgroundColor; expect(bgColor).toBe('yellow'); }); it('should color 2nd <h2> background w/ default color', () => { const dir = des[1].injector.get(HighlightDirective) as HighlightDirective; const bgColor = des[1].nativeElement.style.backgroundColor; expect(bgColor).toBe(dir.defaultColor); }); it('should bind <input> background to value color', () => { // easier to work with nativeElement const input = des[2].nativeElement as HTMLInputElement; expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor'); // dispatch a DOM event so that Angular responds to the input value change. input.value = 'green'; input.dispatchEvent(newEvent('input')); fixture.detectChanges(); expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor'); }); it('bare <h2> should not have a customProperty', () => { expect(bareH2.properties['customProperty']).toBeUndefined(); });
A few techniques are noteworthy:
-
The
By.directive
predicate is a great way to get the elements that have this directive when their element types are unknown. -
The
:not
pseudo-class inBy.css('h2:not([highlight])')
helps find<h2>
elements that do not have the directive.By.css('*:not([highlight])')
finds any element that does not have the directive. -
DebugElement.styles
affords access to element styles even in the absence of a real browser, thanks to theDebugElement
abstraction. But feel free to exploit thenativeElement
when that seems easier or more clear than the abstraction. -
Angular adds a directive to the injector of the element to which it is applied. The test for the default color uses the injector of the second
<h2>
to get itsHighlightDirective
instance and itsdefaultColor
. -
DebugElement.properties
affords access to the artificial custom property that is set by the directive.
Isolated Unit Tests
Testing applications with the help of the Angular testing utilities is the main focus of this guide.
However, it's often more productive to explore the inner logic of application classes with isolated unit tests that don't depend upon Angular. Such tests are often smaller and easier to read, write, and maintain.
They don't carry extra baggage:
- Import from the Angular test libraries.
- Configure a module.
- Prepare dependency injection
providers
. - Call
inject
orasync
orfakeAsync
.
They follow patterns familiar to test developers everywhere:
- Exhibit standard, Angular-agnostic testing techniques.
- Create instances directly with
new
. - Substitute test doubles (stubs, spys, and mocks) for the real dependencies.
Good developers write both kinds of tests for the same application part, often in the same spec file. Write simple isolated unit tests to validate the part in isolation. Write Angular tests to validate the part as it interacts with Angular, updates the DOM, and collaborates with the rest of the application.
Services
Services are good candidates for isolated unit testing. Here are some synchronous and asynchronous unit tests of the FancyService
written without assistance from Angular testing utilities.
// Straight Jasmine - no imports from Angular test libraries describe('FancyService without the TestBed', () => { let service: FancyService; beforeEach(() => { service = new FancyService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getAsyncValue should return async value', (done: DoneFn) => { service.getAsyncValue().then(value => { expect(value).toBe('async value'); done(); }); }); it('#getTimeoutValue should return timeout value', (done: DoneFn) => { service = new FancyService(); service.getTimeoutValue().then(value => { expect(value).toBe('timeout value'); done(); }); }); it('#getObservableValue should return observable value', (done: DoneFn) => { service.getObservableValue().subscribe(value => { expect(value).toBe('observable value'); done(); }); }); });
A rough line count suggests that these isolated unit tests are about 25% smaller than equivalent Angular tests. That's telling but not decisive. The benefit comes from reduced setup and code complexity.
Compare these equivalent tests of FancyService.getTimeoutValue
.
it('#getTimeoutValue should return timeout value', (done: DoneFn) => { service = new FancyService(); service.getTimeoutValue().then(value => { expect(value).toBe('timeout value'); done(); }); });
beforeEach(() => { TestBed.configureTestingModule({ providers: [FancyService] }); }); it('test should wait for FancyService.getTimeoutValue', async(inject([FancyService], (service: FancyService) => { service.getTimeoutValue().then( value => expect(value).toBe('timeout value') ); })));
They have about the same line-count, but the Angular-dependent version has more moving parts including a couple of utility functions (async
and inject
). Both approaches work and it's not much of an issue if you're using the Angular testing utilities nearby for other reasons. On the other hand, why burden simple service tests with added complexity?
Pick the approach that suits you.
Services with dependencies
Services often depend on other services that Angular injects into the constructor. You can test these services without the TestBed
. In many cases, it's easier to create and inject dependencies by hand.
The DependentService
is a simple example:
@Injectable() export class DependentService { constructor(private dependentService: FancyService) { } getValue() { return this.dependentService.getValue(); } }
It delegates its only method, getValue
, to the injected FancyService
.
Here are several ways to test it.
describe('DependentService without the TestBed', () => { let service: DependentService; it('#getValue should return real value by way of the real FancyService', () => { service = new DependentService(new FancyService()); expect(service.getValue()).toBe('real value'); }); it('#getValue should return faked value by way of a fakeService', () => { service = new DependentService(new FakeFancyService()); expect(service.getValue()).toBe('faked value'); }); it('#getValue should return faked value from a fake object', () => { const fake = { getValue: () => 'fake value' }; service = new DependentService(fake as FancyService); expect(service.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a FancyService spy', () => { const fancy = new FancyService(); const stubValue = 'stub value'; const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue); service = new DependentService(fancy); expect(service.getValue()).toBe(stubValue, 'service returned stub value'); expect(spy.calls.count()).toBe(1, 'stubbed method was called once'); expect(spy.calls.mostRecent().returnValue).toBe(stubValue); }); });
The first test creates a FancyService
with new
and passes it to the DependentService
constructor.
However, it's rarely that simple. The injected service can be difficult to create or control. You can mock the dependency, use a dummy value, or stub the pertinent service method with a substitute method that's easy to control.
These isolated unit testing techniques are great for exploring the inner logic of a service or its simple integration with a component class. Use the Angular testing utilities when writing tests that validate how a service interacts with components within the Angular runtime environment.
Pipes
Pipes are easy to test without the Angular testing utilities.
A pipe class has one method, transform
, that manipulates the input value into a transformed output value. The transform
implementation rarely interacts with the DOM. Most pipes have no dependence on Angular other than the @Pipe
metadata and an interface.
Consider a TitleCasePipe
that capitalizes the first letter of each word. Here's a naive implementation with a regular expression.
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'titlecase', pure: false}) /** Transform to Title Case: uppercase the first letter of the words in a string.*/ export class TitleCasePipe implements PipeTransform { transform(input: string): string { return input.length === 0 ? '' : input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() )); } }
Anything that uses a regular expression is worth testing thoroughly. Use simple Jasmine to explore the expected cases and the edge cases.
describe('TitleCasePipe', () => { // This pipe is a pure, stateless function so no need for BeforeEach let pipe = new TitleCasePipe(); it('transforms "abc" to "Abc"', () => { expect(pipe.transform('abc')).toBe('Abc'); }); it('transforms "abc def" to "Abc Def"', () => { expect(pipe.transform('abc def')).toBe('Abc Def'); }); // ... more tests ... });
Write Angular tests too
These are tests of the pipe in isolation. They can't tell if the TitleCasePipe
is working properly as applied in the application components.
Consider adding component tests such as this one:
it('should convert hero name to Title Case', () => { const inputName = 'quick BROWN fox'; const titleCaseName = 'Quick Brown Fox'; // simulate user entering new name into the input box page.nameInput.value = inputName; // dispatch a DOM event so that Angular learns of input value change. page.nameInput.dispatchEvent(newEvent('input')); // Tell Angular to update the output span through the title pipe fixture.detectChanges(); expect(page.nameDisplay.textContent).toBe(titleCaseName); });
Components
Component tests typically examine how a component class interacts with its own template or with collaborating components. The Angular testing utilities are specifically designed to facilitate such tests.
Consider this ButtonComp
component.
@Component({ selector: 'button-comp', template: ` <button (click)="clicked()">Click me!</button> <span>{{message}}</span>` }) export class ButtonComponent { isOn = false; clicked() { this.isOn = !this.isOn; } get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; } }
The following Angular test demonstrates that clicking a button in the template leads to an update of the on-screen message.
it('should support clicking a button', () => { const fixture = TestBed.createComponent(ButtonComponent); const btn = fixture.debugElement.query(By.css('button')); const span = fixture.debugElement.query(By.css('span')).nativeElement; fixture.detectChanges(); expect(span.textContent).toMatch(/is off/i, 'before click'); click(btn); fixture.detectChanges(); expect(span.textContent).toMatch(/is on/i, 'after click'); });
The assertions verify that the data values flow from one HTML control (the <button>
) to the component and from the component back to a different HTML control (the <span>
). A passing test means the component and its template are wired correctly.
Isolated unit tests can more rapidly probe a component at its API boundary, exploring many more conditions with less effort.
Here are a set of unit tests that verify the component's outputs in the face of a variety of component inputs.
describe('ButtonComp', () => { let comp: ButtonComponent; beforeEach(() => comp = new ButtonComponent()); it('#isOn should be false initially', () => { expect(comp.isOn).toBe(false); }); it('#clicked() should set #isOn to true', () => { comp.clicked(); expect(comp.isOn).toBe(true); }); it('#clicked() should set #message to "is on"', () => { comp.clicked(); expect(comp.message).toMatch(/is on/i); }); it('#clicked() should toggle #isOn', () => { comp.clicked(); expect(comp.isOn).toBe(true); comp.clicked(); expect(comp.isOn).toBe(false); }); });
Isolated component tests offer a lot of test coverage with less code and almost no setup. This is even more of an advantage with complex components, which may require meticulous preparation with the Angular testing utilities.
On the other hand, isolated unit tests can't confirm that the ButtonComp
is properly bound to its template or even data bound at all. Use Angular tests for that.
Angular testing utility APIs
This section takes inventory of the most useful Angular testing features and summarizes what they do.
The Angular testing utilities include the TestBed
, the ComponentFixture
, and a handful of functions that control the test environment. The TestBed and ComponentFixture classes are covered separately.
Here's a summary of the stand-alone functions, in order of likely utility:
Function | Description |
---|---|
async | Runs the body of a test ( |
fakeAsync | Runs the body of a test ( |
tick | Simulates the passage of time and the completion of pending asynchronous activities by flushing both timer and micro-task queues within the fakeAsync test zone.
Accepts an optional argument that moves the virtual clock forward by the specified number of milliseconds, clearing asynchronous activities scheduled within that timeframe. See discussion above. |
inject | Injects one or more services from the current |
discardPeriodicTasks | When a In general, a test should end with no queued tasks. When pending timer tasks are expected, call |
flushMicrotasks | When a In general, a test should wait for micro-tasks to finish. When pending microtasks are expected, call |
ComponentFixtureAutoDetect | A provider token for a service that turns on automatic change detection. |
getTestBed | Gets the current instance of the |
TestBed
class summary
The TestBed
class is one of the principal Angular testing utilities. Its API is quite large and can be overwhelming until you've explored it, a little at a time. Read the early part of this guide first to get the basics before trying to absorb the full API.
The module definition passed to configureTestingModule
is a subset of the @NgModule
metadata properties.
type TestModuleMetadata = { providers?: any[]; declarations?: any[]; imports?: any[]; schemas?: Array<SchemaMetadata | any[]>; };
Each override method takes a MetadataOverride<T>
where T
is the kind of metadata appropriate to the method, that is, the parameter of an @NgModule
, @Component
, @Directive
, or @Pipe
.
type MetadataOverride = { add?: T; remove?: T; set?: T; };
The TestBed
API consists of static class methods that either update or reference a global instance of theTestBed
.
Internally, all static methods cover methods of the current runtime TestBed
instance, which is also returned by the getTestBed()
function.
Call TestBed
methods within a beforeEach()
to ensure a fresh start before each individual test.
Here are the most important static methods, in order of likely utility.
Methods | Description |
---|---|
configureTestingModule | The testing shims ( Call |
compileComponents | Compile the testing module asynchronously after you've finished configuring it. You must call this method if any of the testing module components have a After calling |
createComponent | Create an instance of a component of type |
overrideModule | Replace metadata for the given |
overrideComponent | Replace metadata for the given component class, which could be nested deeply within an inner module. |
overrideDirective | Replace metadata for the given directive class, which could be nested deeply within an inner module. |
overridePipe | Replace metadata for the given pipe class, which could be nested deeply within an inner module. |
get | Retrieve a service from the current The What if the service is optional? The src/app/bag/bag.spec.ts
After calling |
initTestEnvironment | Initialize the testing environment for the entire test run. The testing shims ( You may call this method exactly once. If you must change this default in the middle of your test run, call Specify the Angular compiler factory, a |
resetTestEnvironment | Reset the initial test environment, including the default testing module. |
A few of the TestBed
instance methods are not covered by static TestBed
class methods. These are rarely needed.
The ComponentFixture
The TestBed.createComponent<T>
creates an instance of the component T
and returns a strongly typed ComponentFixture
for that component.
The ComponentFixture
properties and methods provide access to the component, its DOM representation, and aspects of its Angular environment.
ComponentFixture
properties
Here are the most important properties for testers, in order of likely utility.
Properties | Description |
---|---|
componentInstance | The instance of the component class created by |
debugElement | The The |
nativeElement | The native DOM element at the root of the component. |
changeDetectorRef | The The |
ComponentFixture
methods
The fixture methods cause Angular to perform certain tasks on the component tree. Call these method to trigger Angular behavior in response to simulated user action.
Here are the most useful methods for testers.
Methods | Description |
---|---|
detectChanges | Trigger a change detection cycle for the component. Call it to initialize the component (it calls Runs |
autoDetectChanges | Set this to When autodetect is The default is |
checkNoChanges | Do a change detection run to make sure there are no pending changes. Throws an exceptions if there are. |
isStable | If the fixture is currently stable, returns |
whenStable | Returns a promise that resolves when the fixture is stable. To resume testing after completion of asynchronous activity or asynchronous change detection, hook that promise. See above. |
destroy | Trigger component destruction. |
DebugElement
The DebugElement
provides crucial insights into the component's DOM representation.
From the test root component's DebugElement
returned by fixture.debugElement
, you can walk (and query) the fixture's entire element and component subtrees.
Here are the most useful DebugElement
members for testers, in approximate order of utility:
Member | Description |
---|---|
nativeElement | The corresponding DOM element in the browser (null for WebWorkers). |
query | Calling |
queryAll | Calling |
injector | The host dependency injector. For example, the root element's component instance injector. |
componentInstance | The element's own component instance, if it has one. |
context | An object that provides parent context for this element. Often an ancestor component instance that governs this element. When an element is repeated within |
children | The immediate
|
parent | The |
name | The element tag name, if it is an element. |
triggerEventHandler | Triggers the event by its name if there is a corresponding listener in the element's If the event lacks a listener or there's some other problem, consider calling |
listeners | The callbacks attached to the component's |
providerTokens | This component's injector lookup tokens. Includes the component itself plus the tokens that the component lists in its |
source | Where to find this element in the source component template. |
references | Dictionary of objects associated with template local variables (e.g. |
The DebugElement.query(predicate)
and DebugElement.queryAll(predicate)
methods take a predicate that filters the source element's subtree for matching DebugElement
.
The predicate is any method that takes a DebugElement
and returns a truthy value. The following example finds all DebugElements
with a reference to a template local variable named "content":
// Filter for DebugElements with a #content reference const contentRefs = el.queryAll( de => de.references['content']);
The Angular By
class has three static methods for common predicates:
-
By.all
- return all elements. -
By.css(selector)
- return elements with matching CSS selectors. -
By.directive(directive)
- return elements that Angular matched to an instance of the directive class.
// Can find DebugElement either by css selector or by directive const h2 = fixture.debugElement.query(By.css('h2')); const directive = fixture.debugElement.query(By.directive(HighlightDirective));
Test environment setup files
Unit testing requires some configuration and bootstrapping that is captured in setup files. The setup files for this guide are provided for you when you follow the Setup instructions. The CLI delivers similar files with the same purpose.
Here's a brief description of this guide's setup files:
The deep details of these files and how to reconfigure them for your needs is a topic beyond the scope of this guide .
File | Description |
---|---|
karma.conf.js | The karma configuration file that specifies which plug-ins to use, which application and test files to load, which browser(s) to use, and how to report test results. It loads three other setup files: |
karma-test-shim.js | This shim prepares karma specifically for the Angular test environment and launches karma itself. It loads the |
systemjs.config.js | SystemJS loads the application and test files. This script tells SystemJS where to find those files and how to load them. It's the same version of |
systemjs.config.extras.js | An optional file that supplements the SystemJS configuration in A stock The sample version for this guide adds the model barrel to the SystemJs |
systemjs.config.extras.js
/** App specific SystemJS configuration */ System.config({ packages: { // barrels 'app/model': {main:'index.js', defaultExtension:'js'}, 'app/model/testing': {main:'index.js', defaultExtension:'js'} } }); |
npm packages
The sample tests are written to run in Jasmine and karma. The two "fast path" setups added the appropriate Jasmine and karma npm packages to the devDependencies
section of the package.json
. They're installed when you run npm install
.
FAQ: Frequently Asked Questions
Why put specs next to the things they test?
It's a good idea to put unit test spec files in the same folder as the application source code files that they test:
- Such tests are easy to find.
- You see at a glance if a part of your application lacks tests.
- Nearby tests can reveal how a part works in context.
- When you move the source (inevitable), you remember to move the test.
- When you rename the source file (inevitable), you remember to rename the test file.
When would I put specs in a test folder?
Application integration specs can test the interactions of multiple parts spread across folders and modules. They don't really belong to any part in particular, so they don't have a natural home next to any one file.
It's often better to create an appropriate folder for them in the tests
directory.
Of course specs that test the test helpers belong in the test
folder, next to their corresponding helper files.
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v4.angular.io/guide/testing