This chapter offers tips and techniques for testing Angular applications. Along the way you will learn some general testing principles and techniques but the focus is on Angular testing.
Contents
- Introduction to Angular Testing
- Setup
- The first karma test
- The Angular Testing Platform (ATP)
- The sample application and its tests
- A simple component test
- Test a component with a service dependency
- Test a component with an async service
- Test a component with an external template
- Test a component with inputs and outputs
- Test a component inside a test host component
- Test a routed component
- Isolated tests
- TestBed API
- FAQ
It’s a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use.
Live examples
The chapter sample code is available as live examples for inspection, experiment, and download.
- The sample application
- The first spec
- The complete application specs
- A grab bag of demonstration specs
Introduction to Angular Testing
You write tests to explore and confirm the behavior of the application.
-
They guard against changes that break existing code (“regressions”).
-
They clarify what the code does both when used as intended and when faced with deviant conditions.
-
They reveal 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.
This chapter assumes that you know something about testing. Don't worry if you don't. There are plenty of books and online resources to get up to speed.
Tools and Technologies
You can write and run Angular tests with a variety of tools and technologies. This chapter 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 Platform |
The Angular Testing Platform creates a test environment and harness for the application code under test. Use it 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 tests while developing the application. It can be an integral part of the application build process. This chapter describes how to setup 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 responds in the browser as expected. |
Setup
Many think writing tests is fun. Few enjoy setting up the test environment. To get to the fun as quickly as possible, the deep details of setup appear later in the chapter (forthcoming). A bare minimum of discussion plus the downloadable source code must suffice for now.
There are two fast paths to getting started.
-
Start a new project following the instructions in the QuickStart github repository.
-
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 chapter, the application and its tests are based on the QuickStart repo.
If youur application was based on the QuickStart repository, you can skip the rest of this section and get on with your first test. The QuickStart repo provides all necessary setup.
Here's brief description of the setup files.
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 modules. This script tells SystemJS where to find the module files and how to load them. It's the same version of the file used by QuickStart-based applications. |
systemjs.config.extras.js |
An optional file that supplements the SystemJS configuration in A stock The sample version for this chapter 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 were installed when you ran npm install
.
The first karma test
Start with a simple test to make sure the setup works properly.
Create a new file called 1st.spec.ts
in the application root folder, app/
Tests written in Jasmine are called specs . The filename extension must be .spec.ts
, the convention adhered to by karma.conf.js
and other tooling.
Put spec files somewhere within the app/
folder. The karma.conf.js
tells karma to look for spec files there, for reasons explained below.
Add the following code to app/1st.spec.ts
.
app/1st.spec.ts
describe('1st tests', () => { it('true is true', () => expect(true).toBe(true)); });
Run karma
Compile and run it in karma from the command line.
The QuickStart repo adds the following command to the scripts
section in package.json
.
"test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
Add that to your package.json
if it's not there already.
Open a terminal or command window and enter
npm test
The command compiles the application and test code a first time. If the compile fails, the command aborts.
If it succeeds, the command re-compiles (this time in watch mode) in one process and starts karma in another. Both processes watch pertinent files and re-run when they detect changes.
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 > tsc && concurrently "tsc -w" "karma start karma.conf.js" [0] 1:37:03 PM - Compilation complete. Watching for file changes. [1] 24 07 2016 13:37:09.310:WARN [karma]: No captured browser, open http://localhost:9876/ [1] 24 07 2016 13:37:09.361:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/ [1] 24 07 2016 13:37:09.370:INFO [launcher]: Starting browser Chrome [1] 24 07 2016 13:37:10.974:INFO [Chrome 51.0.2704]: Connected on socket /#Cf6A5PkvMzjbbtn1AAAA with id 24600087 [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 preceeded 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: Executed 0 of 1 SUCCESS 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 failed 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. It says SUCCESS
when all is well.
If it says FAILED
, scroll up to look for the error or, if that's too painful, pipe the console output to a file and inspect with your favorite editor.
npm test > spec-output.txt
Test debugging
Debug specs in the browser in the same way you debug an application.
- Reveal the karma browser window (hidden earlier).
- Open the browser's “Developer Tools” (F12 or Ctrl-Shift-I).
- Pick the “sources” section
- Open the
1st.spec.ts
test file (Ctrl-P, then start typing the name of the file). - Set a breakpoint in the test
- Refresh the browser … and it stops at the breakpoint.
The Angular Testing Platform (ATP)
Many tests explore how applications classes interact with Angular and the DOM while under Angular's control.
Such tests are easy to write with the help of the Angular Testing Platform (ATP) which consists of the TestBed
class and some helper functions.
Tests written with the Angular Testing Platform are the main focus of this chapter. But they are not the only tests you should write.
Isolated unit tests
You can and should write isolated unit tests for components, directives, pipes, and services. 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 fake constructor parameters as needed, and then probes the test instance API surface.
Isolated tests don't reveal how the class interacts with Angular. In particular, they can't reveal how a component class interacts with its own template or with other components.
Those tests require the Angular Testing Platform.
Testing with the Angular Testing Platform
The Angular Testing Platform consists of the TestBed
class and some helper functions from @angular/core/testing
.
The TestBed is officially experimental and thus subject to change. Consult the API reference for the latest status.
The TestBed
creates an Angular test module — an @NgModule
class — that you configure to produce the module environment for the class you want to test. You tell the TestBed
to create an instance of the test component and probe that instance with tests.
That's the TestBed
in a nutshell.
In practice, you work with the static methods of the TestBed
class. These static methods create and update a fresh hidden TestBed
instance before each Jasmine it
.
You can access that hidden instance anytime by calling getTestBed()
;
This TestBed
instance comes pre-configured with a baseline of default providers and declarables (components, directives, and pipes) that almost everyone needs. This chapter tests a browser application so the default includes the CommonModule
declarables from @angular/common
and the BrowserModule
providers (some of them mocked) from @angular/platform-browser
.
You refine the default test module configuration with application and test specifics so that it can produce an instance of the test component in the Angular environment suitable for your tests.
Start by calling TestBed.configureTestingModule
with an object that looks like @NgModule
metadata. This object defines additional imports, declarations, providers and schemas.
After configuring the TestBed
, tell it to create an instance of the test component and the test fixture you'll need to inspect and control the component's immediate environment.
app/banner.component.spec.ts (simplified)
beforeEach(() => { // refine the test module by declaring the test component TestBed.configureTestingModule({ declarations: [ BannerComponent ], }); // create component and test fixture fixture = TestBed.createComponent(BannerComponent); // get test component from the fixture comp = fixture.componentInstance; });
Angular tests can interact with the HTML in the test DOM, simulate user activity, tell Angular to perform specific task (such as change detection), and see the effects of these actions both in the test component and in the test DOM.
app/banner.component.spec.ts (simplified)
it('should display original title', () => { // trigger data binding to update the view fixture.detectChanges(); // find the title element in the DOM using a CSS selector el = fixture.debugElement.query(By.css('h1')); // confirm the element's content expect(el.nativeElement.textContent).toContain(comp.title); });
A comprehensive review of the TestBed API appears later in the chapter. Let's dive right into Angular testing, starting with with the components of a sample application.
The sample application and its tests
This chapter tests a cut-down version of the Tour of Heroes tutorial app.
The following live example shows how it works and provides the complete source code.
The following live example runs all the tests of this application inside the browser, using the Jasmine Test Runner instead of karma.
It includes the tests discussed in this chapter and additional tests for you to explore. This live example contains both application and test code. It is large and can take several minutes to start. Please be patient.
Test a component
The top of the screen displays application title, presented by the BannerComponent
in app/banner.component.ts
.
app/banner.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-banner', template: '<h1>{{title}}</h1>' }) export class BannerComponent { title = 'Test Tour of Heroes'; }
BannerComponent
has an inline template and an interpolation binding, about as simple as it gets. Probably too simple to be worth testing in real life but perfect for a first encounter with the TestBed
.
The corresponding app/banner-component.spec.ts
sits in the same folder as the component, for reasons explained here;
Start with ES6 import statements to get access to symbols referenced in the spec.
app/banner.component.spec.ts (imports)
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { BannerComponent } from './banner.component';
Here's the setup for the tests followed by observations about the beforeEach
:
app/banner.component.spec.ts (imports)
let comp: BannerComponent; let fixture: ComponentFixture<BannerComponent>; let el: DebugElement; describe('BannerComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ BannerComponent ], // declare the test component }); fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // get title DebugElement by element name el = fixture.debugElement.query(By.css('h1')); }); });
TestBed.configureTestingModule
takes an @NgModule
-like metadata object. This one simply declares the component to test, BannerComponent
.
It lacks imports
because (a) it extends the default test module configuration which already has what BannerComponent
needs and (b) BannerComponent
doesn't interact with any other components.
The configuration could have imported AppModule
(which declares BannerComponent
). But that would lead to tons more configuration in order to support the other components within AppModule
that have nothing to do with BannerComponent
.
TestBed.createComponent
creates an instance of BannerComponent
to test. The 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.
Query the DebugElement
by CSS selector for the <h1>
sub-element that holds the actual title.
createComponent closes configuration
TestBed.createComponent
closes the current TestBed
instance to further configuration. You cannot call any more TestBed
configuration methods, not configureTestModule
nor any of the override...
methods. The TestBed
throws an error if you try.
Do not configure the TestBed
after calling createComponent
.
The tests
Jasmine runs this beforeEach
before each test of which there are two
app/banner.component.spec.ts (tests)
it('should display original title', () => { fixture.detectChanges(); // trigger data binding expect(el.nativeElement.textContent).toContain(comp.title); }); it('should display a different test title', () => { comp.title = 'Test Title'; fixture.detectChanges(); // trigger data binding expect(el.nativeElement.textContent).toContain('Test Title'); });
These tests ask the DebugElement
for the native HTML element to satisfy their expectations.
detectChanges: Angular change detection under 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:
app/banner.component.spec.ts (no detectChanges)
it('no title in the DOM until manually call `detectChanges`', () => { expect(el.nativeElement.textContent).toEqual(''); });
This behavior (or lack of it) is intentional. It gives the tester an opportunity to investigate the state of the component before Angular initiates data binding or calls lifecycle hooks.
Automatic change detection
Some testers prefer that the Angular test environment run change detection automatically. That's possible by configuring the TestBed
with the AutoDetect provider:
app/banner.component.spec.ts (AutoDetect)
fixture = TestBed.configureTestingModule({ declarations: [ BannerComponent ], providers: [ { provide: ComponentFixtureAutoDetect, useValue: true } ] })
Here are three tests that illustrate how auto-detect works.
app/banner.component.spec.ts (AutoDetect Tests)
it('should display original title', () => { // Hooray! No `fixture.detectChanges()` needed expect(el.nativeElement.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.nativeElement.textContent).toContain(oldTitle); }); it('should display updated title after detectChanges', () => { comp.title = 'Test Title'; fixture.detectChanges(); // detect changes explicitly expect(el.nativeElement.textContent).toContain(comp.title); });
The first test shows the benefit of automatic change detection.
The second and third test remind us that Angular does not know about changes to component property values unless Angular itself (or some asynchronous process) makes the change. This is as true in production as it is in test.
In production, external forces rarely change component properties like this, whereas these kinds of probing changes are typical in unit tests. The tester will have to call fixture.detectChanges()
quite often despite having opted into auto detect.
Rather than wonder when the test fixture will or won't perform change detection, the samples in this chapter always call detectChanges()
explicitly.
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
:
app/welcome.component.ts
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; such logic makes this component worth testing. Here's the test module configuration for the spec file, app/welcome.component.spec.ts
:
app/welcome.component.spec.ts
TestBed.configureTestingModule({ declarations: [ WelcomeComponent ], // providers: [ UserService ] // a real service would be a problem! providers: [ {provide: UserService, useValue: fakeUserService } ] });
This time, in addition to declaring the component under test, the configurations sets the providers
list with the dependent UserService
.
This example configures the test module with a fake UserService
.
Provide service fakes
A component under test doesn't have to be injected with real services. In fact, it is usually better if they are fakes. 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 try to ask the user for login credentials and try to reach an authentication server. These behaviors could be hard to intercept. It is far easier to create and register a fake UserService
.
There are many ways to fake a service. This test suit supplies a minimal UserService
that satisfies the needs of the WelcomeComponent
and its tests:
const fakeUserService = { isLoggedIn: true, user: { name: 'Test User'} };
Referencing injected services
The tests need access to the injected (fake) UserService
.
You cannot reference the fakeUserService
object provided to the test module. It does not work! Surprisingly, the instance actually injected into the component is not the same as the provided fakeUserService
object.
Always use an injector to get a reference to an injected service.
Where do you get the injector? Angular has an hierarchical injection system. In a test there can be injectors at multiple levels. The current TestBed
injector creates a top-level injector. The WelcomeComponent
injector is a child of that injector created specifically for the component.
You can get a UserService
from the current TestBed
injector by calling TestBed.get
.
TestBed injector
// UserService provided to the TestBed userService = TestBed.get(UserService);
The inject function is another way to inject one or more services into a test.
That happens to work for testing the WelcomeComponent
because the UserService
instance from the TestBed
is the same as the UserService
instance injected into the component.
That won't always be the case. Be absolutely sure to reference the service instance that the component is actually receiving, Call get
on the component's injector which is fixture.debugElement.injector
:
Component's injector
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService);
Use the component's own injector to get the component's injected service.
Here's the complete, preferred beforeEach
:
app/welcome.component.spec.ts
beforeEach(() => { // fake UserService for test purposes const fakeUserService = { isLoggedIn: true, user: { name: 'Test User'} }; TestBed.configureTestingModule({ declarations: [ WelcomeComponent ], providers: [ {provide: UserService, useValue: fakeUserService } ] }); fixture = TestBed.createComponent(WelcomeComponent); comp = fixture.componentInstance; // UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService); // get the "welcome" element by CSS selector (e.g., by class name) welcomeEl = fixture.debugElement.query(By.css('.welcome')); });
And here are some tests:
app/welcome.component.spec.ts
it('should welcome the user', () => { fixture.detectChanges(); // trigger data binding let content = welcomeEl.nativeElement.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(); // trigger data binding let content = welcomeEl.nativeElement.textContent; expect(content).toContain('Bubba'); }); it('should request login if not logged in', () => { userService.isLoggedIn = false; // welcome message hasn't been shown yet fixture.detectChanges(); // trigger data binding let content = welcomeEl.nativeElement.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 fake UserService
is working. 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 app/shared
folder because the author intends to display Twain quotes on other pages someday. Here is the TwainComponent
.
app/shared/twain.component.ts
@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 at this point. 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 fake such calls. The setup in this app/shared/twain.component.spec.ts
shows one way to do that:
app/shared/twain.component.spec.ts (setup)
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) twainEl = fixture.debugElement.query(By.css('.twain')); });
Spying on the real service
This setup is similar to the welcome.component.spec
setup. But instead of creating a fake service object, it injects the real service (see the test 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 will 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.
Here are the tests with commentary to follow:
app/shared/twain.component.spec.ts (tests)
function getQuote() { return twainEl.nativeElement.textContent; } it('should not show quote before OnInit', () => { expect(getQuote()).toBe('', 'nothing displayed'); expect(spy.calls.any()).toBe(false, 'getQuote not yet called'); }); it('should still not show quote after component initialized', () => { fixture.detectChanges(); // trigger data binding // getQuote service is async => still has not returned with quote expect(getQuote()).toBe('...', 'no quote yet'); expect(spy.calls.any()).toBe(true, 'getQuote called'); }); it('should show quote after getQuote promise (async)', async(() => { fixture.detectChanges(); // trigger data binding fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(getQuote()).toBe(testQuote); }); })); it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { fixture.detectChanges(); // trigger data binding tick(); // wait for async getQuote fixture.detectChanges(); // update view with quote expect(getQuote()).toBe(testQuote); }));
Synchronous tests
The first two tests are synchronous. Neither test can prove that a value from the service will be displayed.
Thanks to the spy, the second test verifies that getQuote
is called. But 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, a least one "tick", before the value becomes available. By that time, the test runner has moved on to the next test in the suite.
The test must become an "async test" ... like the third test
The async function in it
Notice the async
in the third test.
app/shared/twain.component.spec.ts (async test)
it('should show quote after getQuote promise (async)', async(() => { fixture.detectChanges(); // trigger data binding fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(getQuote()).toBe(testQuote); }); }));
The async
function is part of the Angular TestBed feature set. It takes a parameterless function and returns a parameterless function which becomes the argument to the Jasmine it
call.
The body of the async
argument looks much like the body of a normal it
argument. There is nothing obviously asynchronous about it. For example, it doesn't return a promise.
The async
function arranges for the tester's code to run in a special async test zone that almost hides the mechanics of asynchronous execution.
Almost but not completely.
whenStable
The test must wait for the getQuote
promise to resolve.
The getQuote
promise promise resolves in the next turn of the JavaScript engine, thanks to the spy. But a different test implementation of getQuote
could take longer. An integration test might call the real getQuote
, resulting in an XHR request that took many seconds to respond.
This test has no direct access to the promise returned by the call to testService.getQuote
which is private and inaccessible inside TwainComponent
.
Fortunately, the getQuote
promise is accessible to the async test zone which intercepts all promises issued within the async method call.
The ComponentFixture.whenStable
method returns its own promise which resolves when the getQuote
promise completes. In fact, the whenStable promise resolves when all pending asynchronous activities complete ... the definition of "stable".
Then the testing continues. The test kicks off another round of change detection (fixture.detechChanges
) 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.
app/shared/twain.component.spec.ts (fakeAsync test)
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { fixture.detectChanges(); // trigger data binding tick(); // wait for async getQuote fixture.detectChanges(); // update view with quote expect(getQuote()).toBe(testQuote); }));
Notice that fakeAsync
replaces async
as the it
argument. The fakeAsync
function is also part of the Angular TestBed feature set. Like async
, it too takes a parameterless function and returns a parameterless function which becomes the argument to the Jasmine it
call.
The async
function arranges for the tester's code to run in a special fakeAsync test zone.
The key advantage of fakeAsync
is that the test body looks entirely synchronous. There are no promises at all. No then(...)
chains to disrupt the visible flow of control.
There are limitations. For example, you cannot make an XHR call from within a fakeAsync
.
The tick function
Compare the third and fourth tests. Notice that fixture.whenStable
is gone, replaced by tick()
.
The tick
function is a part of the Angular TestBed feature set and a companion to fakeAsync
. It can only be called within a fakeAsync
body.
Calling tick()
simulates the passage of time until all pending asynchronous activities complete, 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 as formerly appeared within 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 fakeAsync
and even async
function greatly simplify Angular asynchronous testing, you can still fallback 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:
app/shared/twain.component.spec.ts (done test)
it('should show quote after getQuote promise (done)', done => { fixture.detectChanges(); // trigger data binding // get the spy promise and wait for it to resolve spy.calls.mostRecent().returnValue.then(() => { fixture.detectChanges(); // update view with quote expect(getQuote()).toBe(testQuote); done(); }); });
Although we have no direct access to the getQuote
promise inside TwainComponent
, the spy does and that makes it possible to wait for getQuote
to finish.
The jasmine.done
technique, while discouraged, may become necessary when neither async
nor fakeAsync
can tolerate a particular asynchronous activity. That's rare but it happens.
Test a component with an external template
The TestBed.createComponent
is a synchronous method. It assumes that everything it could need is already in memory.
That has been true so far. Each tested component's @Component
metadata has a template
property specifying an inline templates. Neither component had a styleUrls
property. Everything necessary to compile them was in memory at test runtime.
The DashboardHeroComponent
is different. It has an external template and external css file, specified in templateUrl
and styleUrls
properties.
app/dashboard/dashboard-hero.component.ts (component)
@Component({ selector: 'dashboard-hero', templateUrl: 'app/dashboard/dashboard-hero.component.html', styleUrls: ['app/dashboard/dashboard-hero.component.css'] }) export class DashboardHeroComponent { @Input() hero: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.next(this.hero); } }
The compiler must read these files from a file system before it can create a component instance.
The TestBed.compileComponents
method asynchronously compiles all the components configured in its current test module. After it completes, external templates and css files, have been "inlined" and TestBed.createComponent
can do its job 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.
The app/dashboard/dashboard-hero.component.spec.ts
demonstrates the pre-compilation process:
app/dashboard/dashboard-hero.component.spec.ts (compileComponents)
// asynch beforeEach beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ DashboardHeroComponent ], }) .compileComponents(); // compile template and css }));
The async function in beforeEach
Notice the async
call in the beforeEach
.
The async
function is part of the Angular TestBed feature set. It takes a parameterless function and returns a parameterless function which becomes the argument to the Jasmine beforeEach
call.
The body of the async
argument looks much like the body of a normal beforEach
argument. There is nothing obviously asynchronous about it. For example, it doesn't return a promise.
The async
function arranges for the tester's code to run in a special async test zone that hides the mechanics of asynchronous execution.
compileComponents
In this example, Testbed.compileComponents
compiles one component, the DashboardComponent
. It's the only declared component in this test module.
Tests later in this chapter have more declared components and some of them import application modules that declare yet more components. Some or all of these components could have external templates and css files. TestBed.compileComponents
compiles them all asynchonously at one time.
The compileComponents
method returns a promise so you can perform additional tasks after it finishes.
compileComponents closes configuration
After compileComponents
runs, the current TestBed
instance is closed to further configuration. You cannot call any more TestBed
configuration methods, not configureTestModule
nor any of the override...
methods. The TestBed
throws an error if you try.
Do not configure the TestBed
after calling compileComponents
. Make compileComponents
the last step before calling TestBed.createInstance
to instantiate the test component.
The DashboardHeroComponent
spec follows the asynchonous beforeEach
with a synchronous beforeEach
that completes the setup steps and runs tests ... as described in the next section.
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 uses 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 tiny example of a component in this role. It displays an individual heroe provided by the DashboardComponent
. Clicking that hero tells the the DashboardComponent
that the user has selected the hero.
The DashboardHeroComponent
is embedded in the DashboardComponent
template like this:
app/dashboard/dashboard.component.html (excerpt)
<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 iteration value and listens for the components selected
event.
Here's the component's definition again:
app/dashboard/dashboard-hero.component.ts (component)
@Component({ selector: 'dashboard-hero', templateUrl: 'app/dashboard/dashboard-hero.component.html', styleUrls: ['app/dashboard/dashboard-hero.component.css'] }) export class DashboardHeroComponent { @Input() hero: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.next(this.hero); } }
While testing a component this simple has little intrinsic value, it's worth knowing how. Three approaches come to mind:
- 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:
app/dashboard/dashboard.component.ts (constructor)
constructor( private router: Router, private heroService: HeroService) { }
The DashboardComponent
depends upon the Angular router and the HeroService
. You'd probably have to fake them both and that's a lot of work. The router is particularly challenging (see below).
The immediate goal is to test the DashboardHeroComponent
, not the DashboardComponent
, and there's no need to work hard unnecessarily. Let's try the second and third options.
Test DashboardHeroComponent stand-alone
Here's the spec file setup.
app/dashboard/dashboard-hero.component.spec.ts (setup)
// asynch 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:
app/dashboard/dashboard-hero.component.spec.ts (name test)
it('should display hero name', () => { const expectedPipedName = expectedHero.name.toUpperCase(); expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); });
It verifies that the hero name is propagated through to template with a binding. There's a twist. The template passes the hero name through the Angular UpperCasePipe
so 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:
app/dashboard/dashboard-hero.component.spec.ts (click test)
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 Angular DebugElement.triggerEventHandler
lets the test raise any data-bound event. In this example, the component's template binds to the hero <div>
.
The test has a reference to that <div>
in heroEl
so triggering the heroEl
click event should cause Angular to call DashboardHeroComponent.click
.
If the component behaves as expected, its selected
property should emit the hero
object, the test detects that emission through its subscription, and the test will pass.
Test a component inside a test host component
In the previous approach the tests themselves played the role of the host DashboardComponent
. A nagging suspicion remains. Will the DashboardHeroComponent
work properly 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:
app/dashboard/dashboard-hero.component.spec.ts (test host)
@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 that records the emitted hero in its selectedHero
property. Later the tests check that property to verify that the DashboardHeroComponent.selected
event really did emit the right hero.
The setup for the test-host tests is similar to the setup for the stand-alone tests:
app/dashboard/dashboard-hero.component.spec.ts (test host setup)
beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both }).compileComponents(); })); beforeEach(() => { // create TestHosComponent 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 test module configuration shows two important differences:
- It declares both the
DashboardHeroComponent
and theTestHostComponent
. - It creates the
TestHostComponent
instead of theDashboardHeroComponent
.
The fixture
returned by createComponent
holds an instance of TestHostComponent
instead of an instance of DashboardHeroComponent
.
Of course 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
app/dashboard/dashboard-hero.component.spec.ts (test-host)
it('should display hero name', () => { const expectedPipedName = testHost.hero.name.toUpperCase(); expect(heroEl.nativeElement.textContent).toContain(expectedPipedName); }); it('should raise selected event when clicked', () => { heroEl.triggerEventHandler('click', null); // 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
.
app/dashboard/dashboard.component.ts (constructor)
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 pre-conditions.
Fortunately, the DashboardComponent
isn't doing much with the Router
app/dashboard/dashboard.component.ts (goToDetail)
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. Faking the router is an easy option. This should do the trick:
app/dashboard/dashboard.component.spec.ts (fakeRouter)
class FakeRouter { navigateByUrl(url: string) { return url; } }
Now we setup the test module with the fakeRouter
and a fake HeroService
and create a test instance of the DashbaordComponent
for subsequent testing.
app/dashboard/dashboard.component.spec.ts (compile and create)
beforeEach( async(() => { TestBed.configureTestingModule({ providers: [ { provide: HeroService, useClass: FakeHeroService }, { provide: Router, useClass: FakeRouter } ] }) .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.
app/dashboard/dashboard.component.spec.ts (navigate test)
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 part of the Angular TestBed feature set. 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:
Component's injector
// 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 configureTestModule
nor any of the override...
methods. The TestBed
throws an error if you try.
Do not configure the TestBed
after calling inject
.
Testing without the Angular Testing Platform
Testing applications with the help of the Angular Testing Platform (ATP) is the main focus of this chapter.
However, it's often more productive to explore the inner logic of application classes with isolated unit tests that don't use the ATP. Such tests are often smaller, easier to read, and easier to write and maintain.
They don't
- import from the Angular test libraries
- configure a module
- prepare dependency injection
providers
- call
inject
orasync
orfakeAsync
They do
- exhibit standard, Angular-agnostic testing techniques
- create instances directly with
new
- use stubs, spys, and mocks to fake 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 vanilla unit testing. Here are some synchronous and asynchronous unit tests of the FancyService
written without assistance from Angular Testing Platform.
app/bag/bag.no-testbed.spec.ts
// 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 => { service.getAsyncValue().then(value => { expect(value).toBe('async value'); done(); }); }); it('#getTimeoutValue should return timeout value', done => { service = new FancyService(); service.getTimeoutValue().then(value => { expect(value).toBe('timeout value'); done(); }); }); it('#getObservableValue should return observable value', done => { service.getObservableValue().subscribe(value => { expect(value).toBe('observable value'); done(); }); }); });
A rough line count suggests that these tests are about 25% smaller than equivalent ATP 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 => { 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. The ATP version has more moving parts, including a couple of helper functions (async
and inject
). Both work and it's not much of an issue if you're using the Angular Testing Platform nearby for other reasons. On the other hand, why burden simple service tests with ATP 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
app/bag/bag.ts
@Injectable() export class DependentService { constructor(private dependentService: FancyService) { } getValue() { return this.dependentService.getValue(); } }
It delegates it's only method, getValue
, to the injected FancyService
.
Here are several ways to test it.
app/bag/bag.no-testbed.spec.ts
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.
It's rarely that simple. The injected service can be difficult to create or control. You can mock the dependency, or use a fake value, or stub the pertinent service method with a substitute method that is 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 Platform 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 Platform (ATP).
A pipe class has one method, transform
, that turns an input to an output. 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 implemented with a regular expression.
app/shared/title-case.pipe.ts
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.
app/shared/title-case.pipe.spec.ts
describe('TitleCasePipe', () => { // This pipe is a pure 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 ATP 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 ATP component tests such as this one.
app/hero/hero-detail.component.spec.ts (pipe test)
it('should convert original hero name to Title Case', () => { expect(page.nameDisplay.textContent).toBe(comp.hero.name); });
Components
Component tests typically examine how a component class interacts with its own template or with collaborating components. The Angular Testing Platform is specifically designed to facilitate such tests.
Consider this ButtonComp
component.
app/bag/bag.ts (ButtonComp)
@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 ATP test demonstrates that clicking a button in the template leads to an update of the on-screen message.
app/bag/bag.spec.ts (ButtonComp)
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'); btn.triggerEventHandler('click', null); fixture.detectChanges(); expect(span.textContent).toMatch(/is on/i, 'after click'); });
The assertions verify the data binding 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 up correctly.
Tests without the ATP 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.
app/bag/bag.no-testbed.spec.ts (ButtonComp)
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 advantage is even more pronounced with complex components that require meticulous preparation with the Angular Testing Platform.
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 ATP tests for that.
Angular Testing Platform APIs
This section takes inventory of the most useful Angular Testing Platform features and summarizes what they do.
The Angular Testing Platform consists of the TestBed
and ComponentFixture
classes plus a handful of functions in the test environment. The TestBed and ComponentFixture classes are covered separately.
Here's a summary of the 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 timer and micro-task queues in the fakeAsync test zone. Accepts an optional argument that moves the virtual clock forward the specified number of milliseconds, clearing asynchronous activities scheduled within that timeframe. See 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 microtasks to finish. When pending microtasks are expected, call |
ComponentFixtureAutoDetect |
A provider token for setting the default auto-changeDetect from its default of |
getTestBed |
Gets the current instance of the |
TestBed Class Summary
The TestBed
class API is quite large and can be overwhelming until you've explored it first a little at a time. Read the early part of this chapter first to get the basics before trying to absorb the full API.
The TestBed is officially experimental and thus subject to change. Consult the API reference for the latest status.
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 overide method takes a MetadataOverride<T>
where T
is the kind of metadata appropriate to the method, 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 that 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 test module asynchronously after you've finished configuring it. You must call this method if any of the test module components have a Once called, the |
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 The service = TestBed.get(FancyService, null); Once called, the |
initTestEnvironment
|
Initialize the testing environment for the entire test run. The testing shims ( This method may be called exactly once. Call Specify the Angular compiler factory, a |
resetTestEnvironment |
Reset the initial test environment including the default test 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 to 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 whether the fixture should try to detect changes automatically. When autodetect is true, the test fixture listens for zone events and calls The default is Calls |
checkNoChanges |
Do a change detection run to make sure there are no pending changes. Throws an exceptions if there are. |
isStable |
Return |
whenStable |
Returns a promise that resolves when the fixture is stable. Hook that promise to resume testing after completion of asynchronous activity or asynchronous change detection. 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 sub-trees.
The DebugElement is officially experimental and thus subject to change. Consult the API reference for the latest status.
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 with in |
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 listner 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.
app/hero/hero-list.component.spec.ts
// 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));
Many custom application directives inject the Renderer
and call one of its set...
methods.
The test environment substitutes the DebugDomRender
for the runtime Renderer
. The DebugDomRender
updates additional dictionary properties of the DebugElement
when something calls a set...
method.
These dictionary properties are primarily of interest to authors of Angular DOM inspection tools but they may provide useful insights to testers as well.
Dictionary | Description |
---|---|
properties |
Updated by |
attributes |
Updated by |
classes |
Updated by |
styles |
Updated by |
Here's an example of Renderer
tests from the live "Specs Bag" sample.
it('DebugDomRender should set attributes, styles, classes, and properties', () => { const fixture = TestBed.createComponent(BankAccountParentComponent); fixture.detectChanges(); const comp = fixture.componentInstance; // the only child is debugElement of the BankAccount component const el = fixture.debugElement.children[0]; const childComp = el.componentInstance as BankAccountComponent; expect(childComp).toEqual(jasmine.any(BankAccountComponent)); expect(el.context).toBe(comp, 'context is the parent component'); expect(el.attributes['account']).toBe(childComp.id, 'account attribute'); expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute'); expect(el.classes['closed']).toBe(true, 'closed class'); expect(el.classes['open']).toBe(false, 'open class'); expect(el.properties['customProperty']).toBe(true, 'customProperty'); expect(el.styles['color']).toBe(comp.color, 'color style'); expect(el.styles['width']).toBe(comp.width + 'px', 'width style'); });
FAQ: Frequently Asked Questions
Why put specs next to the things they test?
We recommend putting unit test spec files in the same folder as the application source code files that they test because
- Such tests are easy to find
- You see at a glance if a part of our 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 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.
Please login to continue.