docs(testing): more scenarios (#2396)

This commit is contained in:
Ward Bell 2016-09-19 19:57:59 -07:00 committed by GitHub
parent 1027e3e9be
commit 7b5244ce87
20 changed files with 487 additions and 178 deletions

View File

@ -30,6 +30,7 @@
<script> <script>
var __spec_files__ = [ var __spec_files__ = [
'app/about.component.spec',
'app/app.component.spec', 'app/app.component.spec',
'app/app.component.router.spec', 'app/app.component.router.spec',
'app/banner.component.spec', 'app/banner.component.spec',

View File

@ -0,0 +1,26 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AboutComponent } from './about.component';
import { HighlightDirective } from './shared/highlight.directive';
let fixture: ComponentFixture<AboutComponent>;
describe('AboutComponent (highlightDirective)', () => {
// #docregion tests
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'));
expect(de.styles['backgroundColor']).toBe('skyblue');
});
// #enddocregion tests
});

View File

@ -1,12 +1,9 @@
// #docregion // #docregion
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
template: ` template: `
<h2 highlight="skyblue">About</h2> <h2 highlight="skyblue">About</h2>
<twain-quote></twain-quote> <twain-quote></twain-quote>
<p>All about this sample</p> <p>All about this sample</p>`
`,
styleUrls: ['app/shared/styles.css']
}) })
export class AboutComponent { } export class AboutComponent { }

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
@NgModule({
imports: [
RouterModule.forRoot([
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
{ path: 'about', component: AboutComponent },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'}
])
],
exports: [ RouterModule ] // re-export the module declarations
})
export class AppRoutingModule { };

View File

@ -1,3 +1,4 @@
<!-- #docregion -->
<app-banner></app-banner> <app-banner></app-banner>
<app-welcome></app-welcome> <app-welcome></app-welcome>

View File

@ -1,108 +1,94 @@
// #docplaster
import { async, ComponentFixture, TestBed import { async, ComponentFixture, TestBed
} from '@angular/core/testing'; } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component'; // #docregion setup-schemas
import { BannerComponent } from './banner.component'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SharedModule } from './shared/shared.module'; // #enddocregion setup-schemas
// #docregion setup-stubs-w-imports
import { Component } from '@angular/core';
// #docregion setup-schemas
import { AppComponent } from './app.component';
// #enddocregion setup-schemas
import { BannerComponent } from './banner.component';
import { RouterLinkStubDirective } from '../testing';
// #docregion setup-schemas
import { RouterOutletStubComponent } from '../testing';
import { Router, RouterStub, RouterLinkDirectiveStub, RouterOutletStubComponent // #enddocregion setup-schemas
} from '../testing'; @Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #enddocregion setup-stubs-w-imports
let comp: AppComponent; let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>; let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => { describe('AppComponent & TestModule', () => {
// #docregion setup-stubs, setup-stubs-w-imports
beforeEach( async(() => { beforeEach( async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ declarations: [
AppComponent, BannerComponent, AppComponent,
RouterLinkDirectiveStub, RouterOutletStubComponent BannerComponent, WelcomeStubComponent,
], RouterLinkStubDirective, RouterOutletStubComponent
providers: [{ provide: Router, useClass: RouterStub }], ]
schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents() .compileComponents()
.then(() => { .then(() => {
fixture = TestBed.createComponent(AppComponent); fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
}); });
})); }));
// #enddocregion setup-stubs, setup-stubs-w-imports
tests(); tests();
}); });
function tests() { //////// Testing w/ NO_ERRORS_SCHEMA //////
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
// #docregion setup-schemas
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
})
it('can instantiate it', () => { .compileComponents()
expect(comp).not.toBeNull(); .then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
}); });
}));
it('can get RouterLinks from template', () => { // #enddocregion setup-schemas
fixture.detectChanges(); tests();
});
const links = fixture.debugElement
// find all elements with an attached FakeRouterLink directive
.queryAll(By.directive(RouterLinkDirectiveStub))
// use injector to get the RouterLink directive instance attached to each element
.map(de => de.injector.get(RouterLinkDirectiveStub) as RouterLinkDirectiveStub);
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', () => {
fixture.detectChanges();
// Heroes RouterLink DebugElement
const heroesLinkDe = fixture.debugElement
.queryAll(By.directive(RouterLinkDirectiveStub))[1];
expect(heroesLinkDe).toBeDefined('should have a 2nd RouterLink');
const link = heroesLinkDe.injector.get(RouterLinkDirectiveStub) as RouterLinkDirectiveStub;
expect(link.navigatedTo).toBeNull('link should not have navigate yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(link.navigatedTo).toBe('/heroes');
});
}
//////// Testing w/ real root module ////// //////// Testing w/ real root module //////
// Best to avoid
// Tricky because we are disabling the router and its configuration // Tricky because we are disabling the router and its configuration
// Better to use RouterTestingModule
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => { describe('AppComponent & AppModule', () => {
beforeEach( async(() => { beforeEach( async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ AppModule ], imports: [ AppModule ]
}) })
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, { .overrideModule(AppModule, {
// Must get rid of `RouterModule.forRoot` to prevent attempt to configure a router remove: {
// Can't remove it because it doesn't have a known type (`forRoot` returns an object) imports: [ AppRoutingModule ]
// therefore, must reset the entire `imports` with just the necessary stuff },
set: { imports: [ SharedModule ]}
})
// Separate override because cannot both `set` and `add/remove` in same override
.overrideModule(AppModule, {
add: { add: {
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ], declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
providers: [{ provide: Router, useClass: RouterStub }]
} }
}) })
@ -117,3 +103,46 @@ describe('AppComponent & AppModule', () => {
tests(); tests();
}); });
function tests() {
let links: RouterLinkStubDirective[];
let linkDes: DebugElement[];
// #docregion test-setup
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);
});
// #enddocregion test-setup
it('can instantiate it', () => {
expect(comp).not.toBeNull();
});
// #docregion tests
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');
});
// #docregion tests
}

View File

@ -1,6 +1,5 @@
// #docregion // #docregion
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
templateUrl: 'app/app.component.html' templateUrl: 'app/app.component.html'

View File

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AboutComponent } from './about.component'; import { AboutComponent } from './about.component';
import { BannerComponent } from './banner.component'; import { BannerComponent } from './banner.component';
import { HeroService, import { HeroService,
@ -19,11 +19,7 @@ import { SharedModule } from './shared/shared.module';
imports: [ imports: [
BrowserModule, BrowserModule,
DashboardModule, DashboardModule,
RouterModule.forRoot([ AppRoutingModule,
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
{ path: 'about', component: AboutComponent },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'}
]),
SharedModule SharedModule
], ],
providers: [ HeroService, TwainService, UserService ], providers: [ HeroService, TwainService, UserService ],

View File

@ -7,10 +7,7 @@ import { Hero, HeroService } from '../model';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
templateUrl: 'app/dashboard/dashboard.component.html', templateUrl: 'app/dashboard/dashboard.component.html',
styleUrls: [ styleUrls: ['app/dashboard/dashboard.component.css']
'app/shared/styles.css',
'app/dashboard/dashboard.component.css'
]
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {

View File

@ -187,7 +187,7 @@ class Page {
const router = compInjector.get(Router); const router = compInjector.get(Router);
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough(); this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
this.navSpy = spyOn(router, 'navigate').and.callThrough(); this.navSpy = spyOn(router, 'navigate');
} }
/** Add page elements after hero arrives */ /** Add page elements after hero arrives */

View File

@ -8,10 +8,7 @@ import { HeroDetailService } from './hero-detail.service';
@Component({ @Component({
selector: 'app-hero-detail', selector: 'app-hero-detail',
templateUrl: 'app/hero/hero-detail.component.html', templateUrl: 'app/hero/hero-detail.component.html',
styleUrls: [ styleUrls: ['app/hero/hero-detail.component.css'],
'app/shared/styles.css',
'app/hero/hero-detail.component.css'
],
providers: [ HeroDetailService ] providers: [ HeroDetailService ]
}) })
export class HeroDetailComponent implements OnInit { export class HeroDetailComponent implements OnInit {

View File

@ -132,7 +132,7 @@ class Page {
// Get the component's injected router and spy on it // Get the component's injected router and spy on it
const router = fixture.debugElement.injector.get(Router); const router = fixture.debugElement.injector.get(Router);
this.navSpy = spyOn(router, 'navigate').and.callThrough(); this.navSpy = spyOn(router, 'navigate');
}; };
} }

View File

@ -6,10 +6,7 @@ import { Hero, HeroService } from '../model';
@Component({ @Component({
selector: 'app-heroes', selector: 'app-heroes',
templateUrl: 'app/hero/hero-list.component.html', templateUrl: 'app/hero/hero-list.component.html',
styleUrls: [ styleUrls: ['app/hero/hero-list.component.css']
'app/shared/styles.css',
'app/hero/hero-list.component.css'
]
}) })
export class HeroListComponent implements OnInit { export class HeroListComponent implements OnInit {
heroes: Promise<Hero[]>; heroes: Promise<Hero[]>;

View File

@ -3,56 +3,98 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive'; import { HighlightDirective } from './highlight.directive';
import { newEvent } from '../../testing';
// Component to test directive // #docregion test-component
@Component({ @Component({
template: ` template: `
<h2 highlight="yellow">Something Yellow</h2> <h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>Something Gray</h2> <h2 highlight>The Default (Gray)</h2>
<h2>Something White</h2> <h2>No Highlight</h2>
` <input #box [highlight]="box.value" value="cyan"/>`
}) })
class TestComponent { } class TestComponent { }
// #enddocregion test-component
////// Tests //////////
describe('HighlightDirective', () => { describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>; let fixture: ComponentFixture<TestComponent>;
let h2Des: DebugElement[]; let des: DebugElement[]; // the three elements w/ the directive
let bareH2: DebugElement; // the <h2> w/o the directive
// #docregion selected-tests
beforeEach(() => { beforeEach(() => {
fixture = TestBed.configureTestingModule({ fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ] declarations: [ HighlightDirective, TestComponent ]
}) })
.createComponent(TestComponent); .createComponent(TestComponent);
h2Des = fixture.debugElement.queryAll(By.css('h2')); 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])'));
}); });
it('should have `HighlightDirective`', () => { // color tests
// The HighlightDirective listed in <h2> tokens means it is attached it('should have three highlighted elements', () => {
expect(h2Des[0].providerTokens).toContain(HighlightDirective, 'HighlightDirective'); expect(des.length).toBe(3);
}); });
it('should color first <h2> background "yellow"', () => { it('should color 1st <h2> background "yellow"', () => {
fixture.detectChanges(); expect(des[0].styles['backgroundColor']).toBe('yellow');
const h2 = h2Des[0].nativeElement as HTMLElement;
expect(h2.style.backgroundColor).toBe('yellow');
}); });
it('should color second <h2> background w/ default color', () => { it('should color 2nd <h2> background w/ default color', () => {
fixture.detectChanges(); const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const h2 = h2Des[1].nativeElement as HTMLElement; expect(des[1].styles['backgroundColor']).toBe(dir.defaultColor);
expect(h2.style.backgroundColor).toBe(HighlightDirective.defaultColor);
}); });
it('should NOT color third <h2> (no directive)', () => { it('should bind <input> background to value color', () => {
// no directive // easier to work with nativeElement
expect(h2Des[2].providerTokens).not.toContain(HighlightDirective, 'HighlightDirective'); 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(); fixture.detectChanges();
const h2 = h2Des[2].nativeElement as HTMLElement; expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
expect(h2.style.backgroundColor).toBe('', 'backgroundColor'); });
// customProperty tests
it('all highlighted elements should have a true customProperty', () => {
const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
expect(allTrue).toBe(true);
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
// #enddocregion selected-tests
// injected directive
// attached HighlightDirective can be injected
it('can inject `HighlightDirective` in 1st <h2>', () => {
const dir = des[0].injector.get(HighlightDirective);
expect(dir).toBeTruthy();
});
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
const dir = bareH2.injector.get(HighlightDirective, null);
expect(dir).toBe(null);
});
// DebugElement.providerTokens
// attached HighlightDirective should be listed in the providerTokens
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
expect(des[0].providerTokens).toContain(HighlightDirective);
});
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
}); });
}); });

View File

@ -1,13 +1,12 @@
// #docregion
import { Directive, ElementRef, Input, OnChanges, Renderer } from '@angular/core'; import { Directive, ElementRef, Input, OnChanges, Renderer } from '@angular/core';
@Directive({ selector: '[highlight]' }) @Directive({ selector: '[highlight]' })
/** /** Set backgroundColor for the attached element to highlight color
* Set backgroundColor for the attached element ton highlight color and * and set the element's customProperty to true */
* set element `customProperty` = true
*/
export class HighlightDirective implements OnChanges { export class HighlightDirective implements OnChanges {
static defaultColor = 'rgb(211, 211, 211)'; // lightgray defaultColor = 'rgb(211, 211, 211)'; // lightgray
@Input('highlight') bgColor: string; @Input('highlight') bgColor: string;
@ -18,7 +17,6 @@ export class HighlightDirective implements OnChanges {
ngOnChanges() { ngOnChanges() {
this.renderer.setElementStyle( this.renderer.setElementStyle(
this.el.nativeElement, 'backgroundColor', this.el.nativeElement, 'backgroundColor',
this.bgColor || HighlightDirective.defaultColor ); this.bgColor || this.defaultColor );
} }
} }

View File

@ -1 +0,0 @@
/* MISSING */

View File

@ -4,19 +4,14 @@ export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router
import { Component, Directive, Injectable, Input } from '@angular/core'; import { Component, Directive, Injectable, Input } from '@angular/core';
import { NavigationExtras } from '@angular/router'; import { NavigationExtras } from '@angular/router';
// #docregion router-link
@Directive({ @Directive({
selector: '[routerLink]', selector: '[routerLink]',
host: { host: {
'(click)': 'onClick()', '(click)': 'onClick()'
'[attr.href]': 'href',
'[class.router-link-active]': 'isRouteActive'
} }
}) })
export class RouterLinkDirectiveStub { export class RouterLinkStubDirective {
isRouteActive = false;
href: string; // the url displayed on the anchor element.
@Input('routerLink') linkParams: any; @Input('routerLink') linkParams: any;
navigatedTo: any = null; navigatedTo: any = null;
@ -24,17 +19,14 @@ export class RouterLinkDirectiveStub {
this.navigatedTo = this.linkParams; this.navigatedTo = this.linkParams;
} }
} }
// #enddocregion router-link
@Component({selector: 'router-outlet', template: ''}) @Component({selector: 'router-outlet', template: ''})
export class RouterOutletStubComponent { } export class RouterOutletStubComponent { }
@Injectable() @Injectable()
export class RouterStub { export class RouterStub {
lastCommand: any[]; navigate(commands: any[], extras?: NavigationExtras) { }
navigate(commands: any[], extras?: NavigationExtras) {
this.lastCommand = commands;
return commands;
}
} }

View File

@ -13,25 +13,63 @@ block includes
a#top a#top
:marked :marked
# Contents # Table of Contents
1. [Introduction to Angular Testing](#testing-intro)
* [Introduction to Angular Testing](#testing-101) <br><br>
* [Setup](#setup) 1. [Setup](#setup)
* [The first karma test](#1st-karma-test) - [setup files](#setup-files): `karma.conf`, `karma-test-shim`, `systemjs.config`
* [The Angular Testing Platform (ATP) ](#atp-intro) - [npm packages](#npm-packages)
* [The sample application and its tests](#sample-app) 1. [The first karma test](#1st-karma-test)
* [A simple component test](#simple-component-test) <br><br>
* [Test a component with a service dependency](#component-with-dependency) 1. [The Angular Testing Platform (ATP) ](#atp-intro)
* [Test a component with an async service](#component-with-async-service) <br><br>
* [Test a component with an external template](#component-with-external-template) 1. [The sample application and its tests](#sample-app)
* [Test a component with inputs and outputs](#component-with-inputs-output) <br><br>
* [Test a component inside a test host component](#component-inside-test-host) 1. [A simple component test](#simple-component-test)
* [Test a routed component](#routed-component) - [_configureTestingModule_](#configure-testing-module)
* [Test a routed component with parameters](#routed-component-w-param) - [_createComponent_](#create-component)
* [Use a _page_ object to simplify setup](#page-object) - [_detectChanges_](#detect-changes)
* [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform") - [_autoDetectChanges_](#auto-detect-changes)
* [_TestBed_ API](#atp-api) 1. [Test a component with a service dependency](#component-with-dependency)
* [FAQ](#faq "Frequently asked questions") <br><br>
1. [Test a component with an async service](#component-with-async-service)
- [spies](#service-spy)
- [_async_](#async)
- [_whenStable_](#when-stable)
- [_fakeAsync_](#async)
- [_tick_](#tick)
- [_jasmine.done_](#jasmine-done)
1. [Test a component with an external template](#component-with-external-template)
- [_async_](#async-in-before-each) in `beforeEach`
- [_compileComponents_](#compile-components)
1. [Test a component with inputs and outputs](#component-with-inputs-output)
<br><br>
1. [Test a component inside a test host component](#component-inside-test-host)
<br><br>
1. [Test a routed component](#routed-component)
- [_inject_](#inject)
1. [Test a routed component with parameters](#routed-component-w-param)
- [_Observable_ test double](#stub-observable)
1. [Use a _page_ object to simplify setup](#page-object)
<br><br>
1. [Test a _RouterOutlet_ component](#router-outlet-component)
- [stubbing unneeded components](#stub-component)
- [Stubbing the _RouterLink_](#router-link-stub)
- [_By.directive_ and injected directives](#by-directive)
1. ["Shallow" component tests with *NO\_ERRORS\_SCHEMA*](#shallow-component-test)
<br><br>
1. [Test an attribute directive](#attribute-directive)
<br><br>
1. [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform")
- [Services](#isolated-service-tests)
- [Pipes](#isolated-pipe-tests)
- [Components](#isolated-component-tests)
1. [_Angular Testing Platform_ API](#atp-api)
- [Stand-alone functions](#atp-api): `async`, `fakeAsync`, etc.
- [_TestBed_](#testbed-class-summary)
- [_ComponentFixture_](#component-fixture-class-summary)
- [_DebugElement_](#debug-element-details)
1. [FAQ](#faq "Frequently asked questions")
:marked :marked
Its a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use. Its a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use.
@ -45,7 +83,7 @@ a#top
a(href="#top").to-top Back to top a(href="#top").to-top Back to top
.l-hr .l-hr
a#testing-101 a#testing-intro
:marked :marked
# Introduction to Angular Testing # Introduction to Angular Testing
@ -146,7 +184,9 @@ a#setup
you can skip the rest of this section and get on with your first test. you can skip the rest of this section and get on with your first test.
The QuickStart repo provides all necessary setup. The QuickStart repo provides all necessary setup.
a#setup-files
:marked :marked
### Setup files
Here's brief description of the setup files. Here's brief description of the setup files.
table(width="100%") table(width="100%")
@ -424,7 +464,6 @@ a#simple-component-test
:marked :marked
# Test a component # Test a component
:marked
The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`. The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`.
+makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.') +makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.')
:marked :marked
@ -436,9 +475,11 @@ a#simple-component-test
Start with ES6 import statements to get access to symbols referenced in the spec. Start with ES6 import statements to get access to symbols referenced in the spec.
+makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.') +makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.')
a#configure-testing-module
:marked :marked
Here's the setup for the tests followed by observations about the `beforeEach`: Here's the setup for the tests followed by observations about the `beforeEach`:
+makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (imports)')(format='.') +makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (setup)')(format='.')
:marked :marked
`TestBed.configureTestingModule` takes an `@NgModule`-like metadata object. `TestBed.configureTestingModule` takes an `@NgModule`-like metadata object.
This one simply declares the component to test, `BannerComponent`. This one simply declares the component to test, `BannerComponent`.
@ -451,6 +492,8 @@ a#simple-component-test
But that would lead to tons more configuration in order to support the other components within `AppModule` But that would lead to tons more configuration in order to support the other components within `AppModule`
that have nothing to do with `BannerComponent`. that have nothing to do with `BannerComponent`.
a#create-component
:marked
`TestBed.createComponent` creates an instance of `BannerComponent` to test. `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 method returns a `ComponentFixture`, a handle on the test environment surrounding the created component.
The fixture provides access to the component instance itself and The fixture provides access to the component instance itself and
@ -458,7 +501,6 @@ a#simple-component-test
Query the `DebugElement` by CSS selector for the `<h1>` sub-element that holds the actual title. Query the `DebugElement` by CSS selector for the `<h1>` sub-element that holds the actual title.
### _createComponent_ closes configuration ### _createComponent_ closes configuration
`TestBed.createComponent` closes the current `TestBed` instance to further configuration. `TestBed.createComponent` closes the current `TestBed` instance to further configuration.
You cannot call any more `TestBed` configuration methods, not `configureTestModule` You cannot call any more `TestBed` configuration methods, not `configureTestModule`
@ -474,7 +516,7 @@ a#simple-component-test
:markdown :markdown
These tests ask the `DebugElement` for the native HTML element to satisfy their expectations. These tests ask the `DebugElement` for the native HTML element to satisfy their expectations.
a#fixture-detect-changes a#detect-changes
:marked :marked
### _detectChanges_: Angular change detection under test ### _detectChanges_: Angular change detection under test
@ -498,7 +540,7 @@ a#fixture-detect-changes
It gives the tester an opportunity to investigate the state of It gives the tester an opportunity to investigate the state of
the component _before Angular initiates data binding or calls lifecycle hooks_. the component _before Angular initiates data binding or calls lifecycle hooks_.
a#automatic-change-detection a#auto-detect-changes
:marked :marked
### Automatic change detection ### Automatic change detection
Some testers prefer that the Angular test environment run change detection automatically. Some testers prefer that the Angular test environment run change detection automatically.
@ -805,7 +847,7 @@ a#component-with-external-template
The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process: The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process:
+makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.') +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.')
a#async-fn-in-before-each a#async-in-before-each
:marked :marked
## The _async_ function in _beforeEach_ ## The _async_ function in _beforeEach_
@ -992,7 +1034,7 @@ a#routed-component
Stubbing the router with a test implementation is an easy option. This should do the trick: Stubbing the router with a test implementation is an easy option. This should do the trick:
+makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'router-stub', 'app/dashboard/dashboard.component.spec.ts (Router Stub)')(format='.') +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'router-stub', 'app/dashboard/dashboard.component.spec.ts (Router Stub)')(format='.')
:marked :marked
Now we setup the testing module with test stubs for the `Router` and `HeroService` and Now we setup the testing module with the test stubs for the `Router` and `HeroService` and
create a test instance of the `DashbaordComponent` for subsequent testing. create a test instance of the `DashbaordComponent` for subsequent testing.
+makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.') +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.')
:marked :marked
@ -1169,10 +1211,187 @@ figure.image-display
Here are a few more `HeroDetailComponent` tests to drive the point home. Here are a few more `HeroDetailComponent` tests to drive the point home.
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'selected-tests', 'app/hero/hero-detail.component.spec.ts (selected tests)')(format='.') +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'selected-tests', 'app/hero/hero-detail.component.spec.ts (selected tests)')(format='.')
:marked :marked
a(href="#top").to-top Back to top
.l-hr
a#router-outlet-component
:marked
# 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.
a#app-component-html
+makeExample('testing/ts/app/app.component.html', '', 'app/app.component.html')(format='.')
:marked
The component class does nothing.
+makeExample('testing/ts/app/app.component.ts', '', 'app/app.component.ts')(format='.')
.l-sub-section
:marked
This component may not seem worth testing but it's instructive to show how to test it.
:marked
Unit tests can confirm that the anchors are wired properly without engaging the router.
a#stub-component
:marked
### Stubbing unneeded components
The test setup should look familiar
+makeExample('testing/ts/app/app.component.spec.ts', 'setup-stubs', 'app/app.component.spec.ts (Stub Setup)')(format='.')
:marked
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.
The `RouterOutletStubComponent` (in `testing/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.
a#router-link-stub
:marked
### Stubbing the _RouterLink_
The `RouterLinkStubDirective` contributes substantively to the test
+makeExample('testing/ts/testing/router-stubs.ts', 'router-link', 'testing/router-stubs.ts (RouterLinkStubDirective)')(format='.')
:marked
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.
a#by-directive
a#inject-directive
:marked
### _By.directive_ and injected directives
A little more setup triggers the initial data binding and gets references to the navigation links:
+makeExample('testing/ts/app/app.component.spec.ts', 'test-setup', 'app/app.component.spec.ts (test setup)')(format='.')
:marked
Two points of special interest:
1. You can locate elements _by directive_, using `By.directive`, not just by css selectors.
1. You can use the component's dependency injector to get an attached directive because
Angular always adds attached directives to the component's injector.
a#app-component-tests
:marked
Here are some tests that leverage this setup:
+makeExample('testing/ts/app/app.component.spec.ts', 'tests', 'app/app.component.spec.ts (selected tests)')(format='.')
.l-sub-section
:marked
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 chapter.
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.
a(href="#top").to-top Back to top a(href="#top").to-top Back to top
.l-hr .l-hr
a#shallow-component-test
:marked
# "Shallow component tests" with *NO\_ERRORS\_SCHEMA*
The [previous setup](#stub-component) 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_](#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.
+makeTabs('testing/ts/app/app.component.spec.ts, testing/ts/app/app.component.spec.ts',
'setup-schemas, setup-stubs-w-imports',
'app/app.component.spec.ts (NO_ERRORS_SCHEMA), app/app.component.spec.ts (Stubs)')(format='.')
:marked
The _only_ declarations are the _component-under-test_ (`AppComponent`) and the `RouterLinkStubDirective`
that contributes actively to the tests.
The [tests in this example](#app-component-tests) are unchanged.
.alert.is-important
:marked
_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.
a(href="#top").to-top Back to top
.l-hr
a#attribute-directive
:marked
# 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.
+makeExample('testing/ts/app/shared/highlight.directive.ts', '', 'app/shared/highlight.directive.ts')(format='.')
:marked
It's used throughout the application, perhaps most simply in the `AboutComponent`:
+makeExample('testing/ts/app/about.component.ts', '', 'app/about.component.ts')(format='.')
:marked
Testing the specific use of the `HighlightDirective` within the `AboutComponent` requires only the
techniques explored above (in particular the ["Shallow test"](#shallow-component-test) approach).
+makeExample('testing/ts/app/about.component.spec.ts', 'tests', 'app/about.component.spec.ts')(format='.')
:marked
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](#isolated-tests) might be helpful.
But attribute directives like this one tend to manipulate the DOM.
Isolated tests don't and therefore don't 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.
+makeExample('testing/ts/app/shared/highlight.directive.spec.ts', 'test-component', 'app/shared/highlight.directive.spec.ts (test component)')(format='.')
figure.image-display
img(src='/resources/images/devguide/testing/highlight-directive-spec.png' width="200px" alt="HighlightDirective spec in action")
.l-sub-section
:marked
The `<input>` case binds the `HighlightDirective` 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.
:marked
Here are some tests of this component:
+makeExample('testing/ts/app/shared/highlight.directive.spec.ts', 'selected-tests', 'app/shared/highlight.directive.spec.ts (selected tests)')
:marked
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 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:not" target="_blank">`:not` pseudo-class</a>
in `By.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 the `DebugElement` abstraction.
But feel free to exploit the `nativeElement` 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 2nd `<h2>` to get its `HighlightDirective` instance
and its `defaultColor`.
* `DebugElement.properties` affords access to the artificial custom property that is set by the directive.
a(href="#top").to-top Back to top
.l-hr
a#isolated-tests a#isolated-tests
a#testing-without-atp a#testing-without-atp
:marked :marked
@ -1204,9 +1423,10 @@ a#testing-without-atp
Write _Angular_ tests to validate the part as it interacts with Angular, Write _Angular_ tests to validate the part as it interacts with Angular,
updates the DOM, and collaborates with the rest of the application. updates the DOM, and collaborates with the rest of the application.
a#isolated-service-tests
:marked :marked
## Services ## Services
Services are good candidates for vanilla unit testing. Services are good candidates for isolated unit testing.
Here are some synchronous and asynchronous unit tests of the `FancyService` Here are some synchronous and asynchronous unit tests of the `FancyService`
written without assistance from Angular Testing Platform. written without assistance from Angular Testing Platform.
@ -1254,6 +1474,8 @@ a#testing-without-atp
Use the Angular Testing Platform when writing tests that validate how a service interacts with components Use the Angular Testing Platform when writing tests that validate how a service interacts with components
_within the Angular runtime environment_. _within the Angular runtime environment_.
a#isolated-pipe-tests
:marked
## Pipes ## Pipes
Pipes are easy to test without the Angular Testing Platform (ATP). Pipes are easy to test without the Angular Testing Platform (ATP).
@ -1322,9 +1544,9 @@ a#atp-api
This section takes inventory of the most useful _Angular Testing Platform_ features and summarizes what they do. 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 _Angular Testing Platform_ consists of the `TestBed` and `ComponentFixture` classes plus a handful of functions in the test environment.
The [_TestBed_](#testbed-api-summary) and [_ComponentFixture_](#componentfixture-api-summary) classes are covered separately. The [_TestBed_](#testbed-api-summary) and [_ComponentFixture_](#component-fixture-api-summary) classes are covered separately.
Here's a summary of the functions, in order of likely utility: Here's a summary of the stand-alone functions, in order of likely utility:
table table
tr tr
@ -1537,8 +1759,8 @@ table
if you absolutely need to change this default in the middle of your test run. if you absolutely need to change this default in the middle of your test run.
Specify the Angular compiler factory, a `PlatformRef`, and a default Angular testing module. Specify the Angular compiler factory, a `PlatformRef`, and a default Angular testing module.
Alternatives for non-browser platforms are available from Alternatives for non-browser platforms are available in the general form
`angular2/platform/testing/<platform_name>`. `@angular/platform-<platform_name>/testing/<platform_name>`.
tr tr
td(style="vertical-align: top") <code>resetTestEnvironment</code> td(style="vertical-align: top") <code>resetTestEnvironment</code>
td td
@ -1549,7 +1771,7 @@ table
A few of the `TestBed` instance methods are not covered by static `TestBed` _class_ methods. A few of the `TestBed` instance methods are not covered by static `TestBed` _class_ methods.
These are rarely needed. These are rarely needed.
a#componentfixture-api-summary a#component-fixture-api-summary
:marked :marked
## The _ComponentFixture_ ## The _ComponentFixture_
@ -1560,7 +1782,7 @@ a#componentfixture-api-summary
The `ComponentFixture` properties and methods provide access to the component, The `ComponentFixture` properties and methods provide access to the component,
its DOM representation, and aspects of its Angular environment. its DOM representation, and aspects of its Angular environment.
a#componentfixture-properties a#component-fixture-properties
:marked :marked
### _ComponentFixture_ properties ### _ComponentFixture_ properties
@ -1582,7 +1804,7 @@ table
The `DebugElement` associated with the root element of the component. The `DebugElement` associated with the root element of the component.
The `debugElement` provides insight into the component and its DOM element during test and debugging. The `debugElement` provides insight into the component and its DOM element during test and debugging.
It's a critical property for testers. The most interesting members are covered [below](#debugelement-details). It's a critical property for testers. The most interesting members are covered [below](#debug-element-details).
tr tr
td(style="vertical-align: top") <code>nativeElement</code> td(style="vertical-align: top") <code>nativeElement</code>
td td
@ -1598,7 +1820,7 @@ table
component that has the `ChangeDetectionStrategy.OnPush` component that has the `ChangeDetectionStrategy.OnPush`
or the component's change detection is under your programmatic control. or the component's change detection is under your programmatic control.
a#componentfixture-methods a#component-fixture-methods
:marked :marked
### _ComponentFixture_ methods ### _ComponentFixture_ methods
@ -1665,7 +1887,7 @@ table
:marked :marked
Trigger component destruction. Trigger component destruction.
a#debugelement-details a#debug-element-details
:marked :marked
### _DebugElement_ ### _DebugElement_
@ -1855,7 +2077,7 @@ a(href="#top").to-top Back to top
General General
* [When are end-to-end (e2e) tests a good choice?](#q-when-e2e) * [When are end-to-end (e2e) tests a good choice?](#q-when-e2e)
* [When to use the _TestBed_?](#q-why-testbed) * [When to use the _TestBed_?](#q-why-testbed)
* [When to write vanilla tests without the _TestBed_?](#q-when-no-testbed) * [When to write isolated unit tests without the _TestBed_?](#q-when-no-testbed)
* [When can I skip _TestBed.compileComponents_?](#q-when-no-compile-components) * [When can I skip _TestBed.compileComponents_?](#q-when-no-compile-components)
* [Why must _TestBed.compileComponents_ be called last?](#q-why-compile-components-is-last) * [Why must _TestBed.compileComponents_ be called last?](#q-why-compile-components-is-last)
* [Why must _inject_ be called last?](#q-why-last-last) * [Why must _inject_ be called last?](#q-why-last-last)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB