docs(testing): feedback from Joe Eames/add more scenarios (#2391)
This commit is contained in:
parent
b9733fdbb6
commit
84b5297cc2
|
@ -42,7 +42,7 @@
|
|||
'app/model/hero.spec',
|
||||
'app/model/http-hero.service.spec',
|
||||
'app/shared/title-case.pipe.spec',
|
||||
'app/twain.component.spec',
|
||||
'app/shared/twain.component.spec',
|
||||
'app/welcome.component.spec'
|
||||
];
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { AppComponent } from './app.component';
|
|||
import { BannerComponent } from './banner.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
import { Router, FakeRouter, FakeRouterLinkDirective, FakeRouterOutletComponent
|
||||
import { Router, RouterStub, RouterLinkDirectiveStub, RouterOutletStubComponent
|
||||
} from '../testing';
|
||||
|
||||
|
||||
|
@ -20,9 +20,9 @@ describe('AppComponent & TestModule', () => {
|
|||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent, BannerComponent,
|
||||
FakeRouterLinkDirective, FakeRouterOutletComponent
|
||||
RouterLinkDirectiveStub, RouterOutletStubComponent
|
||||
],
|
||||
providers: [{ provide: Router, useClass: FakeRouter }],
|
||||
providers: [{ provide: Router, useClass: RouterStub }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
||||
|
@ -49,9 +49,9 @@ function tests() {
|
|||
|
||||
const links = fixture.debugElement
|
||||
// find all elements with an attached FakeRouterLink directive
|
||||
.queryAll(By.directive(FakeRouterLinkDirective))
|
||||
.queryAll(By.directive(RouterLinkDirectiveStub))
|
||||
// use injector to get the RouterLink directive instance attached to each element
|
||||
.map(de => de.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective);
|
||||
.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');
|
||||
|
@ -63,11 +63,11 @@ function tests() {
|
|||
|
||||
// Heroes RouterLink DebugElement
|
||||
const heroesLinkDe = fixture.debugElement
|
||||
.queryAll(By.directive(FakeRouterLinkDirective))[1];
|
||||
.queryAll(By.directive(RouterLinkDirectiveStub))[1];
|
||||
|
||||
expect(heroesLinkDe).toBeDefined('should have a 2nd RouterLink');
|
||||
|
||||
const link = heroesLinkDe.injector.get(FakeRouterLinkDirective) as FakeRouterLinkDirective;
|
||||
const link = heroesLinkDe.injector.get(RouterLinkDirectiveStub) as RouterLinkDirectiveStub;
|
||||
|
||||
expect(link.navigatedTo).toBeNull('link should not have navigate yet');
|
||||
|
||||
|
@ -101,8 +101,8 @@ describe('AppComponent & AppModule', () => {
|
|||
// Separate override because cannot both `set` and `add/remove` in same override
|
||||
.overrideModule(AppModule, {
|
||||
add: {
|
||||
declarations: [ FakeRouterLinkDirective, FakeRouterOutletComponent ],
|
||||
providers: [{ provide: Router, useClass: FakeRouter }]
|
||||
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ],
|
||||
providers: [{ provide: Router, useClass: RouterStub }]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -12,11 +12,11 @@ import { Router } from '@angular/router';
|
|||
import { DashboardComponent } from './dashboard.component';
|
||||
import { DashboardModule } from './dashboard.module';
|
||||
|
||||
// #docregion fake-router
|
||||
class FakeRouter {
|
||||
// #docregion router-stub
|
||||
class RouterStub {
|
||||
navigateByUrl(url: string) { return url; }
|
||||
}
|
||||
// #enddocregion fake-router
|
||||
// #enddocregion router-stub
|
||||
|
||||
beforeEach ( addMatchers );
|
||||
|
||||
|
@ -73,7 +73,7 @@ function compileAndCreate() {
|
|||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: FakeRouter }
|
||||
{ provide: Router, useClass: RouterStub }
|
||||
]
|
||||
})
|
||||
.compileComponents().then(() => {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<!-- #docregion -->
|
||||
<div *ngIf="hero">
|
||||
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
|
||||
<div>
|
||||
<label>id: </label>{{hero.id}}</div>
|
||||
<div>
|
||||
<label>name: </label>
|
||||
<input [(ngModel)]="hero.name" placeholder="name" />
|
||||
<label for="name">name: </label>
|
||||
<input id="name" [(ngModel)]="hero.name" placeholder="name" />
|
||||
</div>
|
||||
<button (click)="save()">Save</button>
|
||||
<button (click)="cancel()">Cancel</button>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
import { Hero } from '../model';
|
||||
|
||||
import { FakeActivatedRoute } from '../../testing';
|
||||
import { ActivatedRouteStub } from '../../testing';
|
||||
|
||||
////////// Tests ////////////////////
|
||||
|
||||
describe('HeroDetailComponent - no TestBed', () => {
|
||||
let activatedRoute: FakeActivatedRoute;
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let comp: HeroDetailComponent;
|
||||
let expectedHero: Hero;
|
||||
let hds: any;
|
||||
|
@ -14,7 +14,7 @@ describe('HeroDetailComponent - no TestBed', () => {
|
|||
|
||||
beforeEach( done => {
|
||||
expectedHero = new Hero(42, 'Bubba');
|
||||
activatedRoute = new FakeActivatedRoute();
|
||||
activatedRoute = new ActivatedRouteStub();
|
||||
activatedRoute.testParams = { id: expectedHero.id };
|
||||
|
||||
router = jasmine.createSpyObj('router', ['navigate']);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// #docplaster
|
||||
import {
|
||||
async, ComponentFixture, fakeAsync, inject, TestBed, tick
|
||||
} from '@angular/core/testing';
|
||||
|
@ -7,7 +8,7 @@ import { DebugElement } from '@angular/core';
|
|||
|
||||
import {
|
||||
addMatchers, newEvent,
|
||||
ActivatedRoute, FakeActivatedRoute, Router, FakeRouter
|
||||
ActivatedRoute, ActivatedRouteStub, Router, RouterStub
|
||||
} from '../../testing';
|
||||
|
||||
import { HEROES, FakeHeroService } from '../model/testing';
|
||||
|
@ -18,7 +19,7 @@ import { HeroDetailService } from './hero-detail.service';
|
|||
import { Hero, HeroService } from '../model';
|
||||
|
||||
////// Testing Vars //////
|
||||
let activatedRoute: FakeActivatedRoute;
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let comp: HeroDetailComponent;
|
||||
let fixture: ComponentFixture<HeroDetailComponent>;
|
||||
let page: Page;
|
||||
|
@ -29,7 +30,7 @@ describe('HeroDetailComponent', () => {
|
|||
|
||||
beforeEach( async(() => {
|
||||
addMatchers();
|
||||
activatedRoute = new FakeActivatedRoute();
|
||||
activatedRoute = new ActivatedRouteStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ HeroModule ],
|
||||
|
@ -40,12 +41,13 @@ describe('HeroDetailComponent', () => {
|
|||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: FakeRouter},
|
||||
{ provide: Router, useClass: RouterStub},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
// #docregion route-good-id
|
||||
describe('when navigate to hero id=' + HEROES[0].id, () => {
|
||||
let expectedHero: Hero;
|
||||
|
||||
|
@ -55,51 +57,53 @@ describe('HeroDetailComponent', () => {
|
|||
createComponent();
|
||||
}));
|
||||
|
||||
// #docregion selected-tests
|
||||
it('should display that hero\'s name', () => {
|
||||
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
||||
});
|
||||
// #enddocregion route-good-id
|
||||
|
||||
it('should navigate when click cancel', () => {
|
||||
page.cancelBtn.triggerEventHandler('click', null);
|
||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
});
|
||||
|
||||
it('should save when click save', () => {
|
||||
it('should save when click save but not navigate immediately', () => {
|
||||
page.saveBtn.triggerEventHandler('click', null);
|
||||
expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
|
||||
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
|
||||
});
|
||||
|
||||
it('should navigate when click click save resolves', fakeAsync(() => {
|
||||
it('should navigate when click save and save resolves', fakeAsync(() => {
|
||||
page.saveBtn.triggerEventHandler('click', null);
|
||||
tick(); // waits for async save to "complete" before navigating
|
||||
tick(); // wait for async save to "complete" before navigating
|
||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
}));
|
||||
|
||||
|
||||
// #docregion title-case-pipe
|
||||
it('should convert original hero name to Title Case', () => {
|
||||
expect(page.nameDisplay.textContent).toBe(comp.hero.name);
|
||||
});
|
||||
// #enddocregion title-case-pipe
|
||||
|
||||
it('should convert hero name to Title Case', fakeAsync(() => {
|
||||
const inputName = 'quick BROWN fox';
|
||||
const expectedName = 'Quick Brown Fox';
|
||||
const titleCaseName = 'Quick Brown Fox';
|
||||
|
||||
// simulate user entering new name in input
|
||||
// simulate user entering new name into the input box
|
||||
page.nameInput.value = inputName;
|
||||
|
||||
// dispatch a DOM event so that Angular learns of input value change.
|
||||
// detectChanges() makes ngModel push input value to component property
|
||||
// and Angular updates the output span
|
||||
page.nameInput.dispatchEvent(newEvent('input'));
|
||||
|
||||
// detectChanges() makes [(ngModel)] push input value to component property
|
||||
// and Angular updates the output span through the title pipe
|
||||
fixture.detectChanges();
|
||||
expect(page.nameDisplay.textContent).toBe(expectedName, 'hero name display');
|
||||
expect(comp.hero.name).toBe(inputName, 'comp.hero.name');
|
||||
|
||||
expect(page.nameDisplay.textContent).toBe(titleCaseName);
|
||||
}));
|
||||
|
||||
// #enddocregion title-case-pipe
|
||||
// #enddocregion selected-tests
|
||||
// #docregion route-good-id
|
||||
});
|
||||
// #enddocregion route-good-id
|
||||
|
||||
// #docregion route-no-id
|
||||
describe('when navigate with no hero id', () => {
|
||||
beforeEach( async( createComponent ));
|
||||
|
||||
|
@ -111,7 +115,9 @@ describe('HeroDetailComponent', () => {
|
|||
expect(page.nameDisplay.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
// #enddocregion route-no-id
|
||||
|
||||
// #docregion route-bad-id
|
||||
describe('when navigate to non-existant hero id', () => {
|
||||
beforeEach( async(() => {
|
||||
activatedRoute.testParams = { id: 99999 };
|
||||
|
@ -123,6 +129,7 @@ describe('HeroDetailComponent', () => {
|
|||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||
});
|
||||
});
|
||||
// #enddocregion route-bad-id
|
||||
|
||||
///////////////////////////
|
||||
|
||||
|
@ -145,22 +152,24 @@ describe('HeroDetailComponent', () => {
|
|||
|
||||
/////////// Helpers /////
|
||||
|
||||
// #docregion create-component
|
||||
/** Create the HeroDetailComponent, initialize it, set test variables */
|
||||
function createComponent() {
|
||||
fixture = TestBed.createComponent(HeroDetailComponent);
|
||||
comp = fixture.componentInstance;
|
||||
page = new Page();
|
||||
|
||||
// change detection triggers ngOnInit which gets a hero
|
||||
// 1st change detection triggers ngOnInit which gets a hero
|
||||
fixture.detectChanges();
|
||||
return fixture.whenStable().then(() => {
|
||||
// got the hero and updated component
|
||||
// change detection updates the view
|
||||
// 2nd change detection displays the async-fetched hero
|
||||
fixture.detectChanges();
|
||||
page.addPageElements();
|
||||
});
|
||||
}
|
||||
// #enddocregion create-component
|
||||
|
||||
// #docregion page
|
||||
class Page {
|
||||
gotoSpy: jasmine.Spy;
|
||||
navSpy: jasmine.Spy;
|
||||
|
@ -173,19 +182,19 @@ class Page {
|
|||
|
||||
constructor() {
|
||||
// Use component's injector to see the services it injected.
|
||||
let compInjector = fixture.debugElement.injector;
|
||||
let hds = compInjector.get(HeroDetailService);
|
||||
let router = compInjector.get(Router);
|
||||
const compInjector = fixture.debugElement.injector;
|
||||
const hds = compInjector.get(HeroDetailService);
|
||||
const router = compInjector.get(Router);
|
||||
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
|
||||
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
|
||||
this.navSpy = spyOn(router, 'navigate').and.callThrough();
|
||||
}
|
||||
|
||||
/** Add page elements after page initializes */
|
||||
/** Add page elements after hero arrives */
|
||||
addPageElements() {
|
||||
if (comp.hero) {
|
||||
// have a hero so these DOM elements can be reached
|
||||
let buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
// have a hero so these elements are now in the DOM
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
this.saveBtn = buttons[0];
|
||||
this.cancelBtn = buttons[1];
|
||||
this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement;
|
||||
|
@ -193,4 +202,4 @@ class Page {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #enddocregion page
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import 'rxjs/add/operator/pluck';
|
||||
|
||||
import { Hero } from '../model';
|
||||
import { HeroDetailService } from './hero-detail.service';
|
||||
|
@ -16,20 +17,24 @@ import { HeroDetailService } from './hero-detail.service';
|
|||
export class HeroDetailComponent implements OnInit {
|
||||
@Input() hero: Hero;
|
||||
|
||||
// #docregion ctor
|
||||
constructor(
|
||||
private heroDetailService: HeroDetailService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router) {
|
||||
}
|
||||
// #enddocregion ctor
|
||||
|
||||
ngOnInit() {
|
||||
let id = this.route.snapshot.params['id'];
|
||||
// #docregion ng-on-init
|
||||
ngOnInit(): void {
|
||||
// get hero when `id` param changes
|
||||
this.route.params.pluck<string>('id')
|
||||
.forEach(id => this.getHero(id))
|
||||
.catch(() => this.hero = new Hero()); // no id; should edit new hero
|
||||
}
|
||||
// #enddocregion ng-on-init
|
||||
|
||||
// tslint:disable-next-line:triple-equals
|
||||
if (id == undefined) {
|
||||
// no id; act as if is new
|
||||
this.hero = new Hero();
|
||||
} else {
|
||||
private getHero(id: string): void {
|
||||
this.heroDetailService.getHero(id).then(hero => {
|
||||
if (hero) {
|
||||
this.hero = hero;
|
||||
|
@ -38,9 +43,8 @@ export class HeroDetailComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
save(): void {
|
||||
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick
|
|||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { addMatchers, newEvent, Router, FakeRouter
|
||||
import { addMatchers, newEvent, Router, RouterStub
|
||||
} from '../../testing';
|
||||
|
||||
import { HEROES, FakeHeroService } from '../model/testing';
|
||||
|
@ -28,7 +28,7 @@ describe('HeroListComponent', () => {
|
|||
imports: [HeroModule],
|
||||
providers: [
|
||||
{ provide: HeroService, useClass: FakeHeroService },
|
||||
{ provide: Router, useClass: FakeRouter}
|
||||
{ provide: Router, useClass: RouterStub}
|
||||
]
|
||||
})
|
||||
.compileComponents()
|
||||
|
|
|
@ -13,15 +13,20 @@ describe('WelcomeComponent', () => {
|
|||
let userService: UserService; // the actually injected service
|
||||
let welcomeEl: DebugElement; // the element with the welcome message
|
||||
|
||||
let userServiceStub: {
|
||||
isLoggedIn: boolean;
|
||||
user: { name: string}
|
||||
};
|
||||
|
||||
// #docregion setup
|
||||
beforeEach(() => {
|
||||
// fake UserService for test purposes
|
||||
// #docregion fake-userservice
|
||||
const fakeUserService = {
|
||||
// stub UserService for test purposes
|
||||
// #docregion user-service-stub
|
||||
userServiceStub = {
|
||||
isLoggedIn: true,
|
||||
user: { name: 'Test User'}
|
||||
};
|
||||
// #enddocregion fake-userservice
|
||||
// #enddocregion user-service-stub
|
||||
|
||||
// #docregion config-test-module
|
||||
TestBed.configureTestingModule({
|
||||
|
@ -29,7 +34,7 @@ describe('WelcomeComponent', () => {
|
|||
// #enddocregion setup
|
||||
// providers: [ UserService ] // a real service would be a problem!
|
||||
// #docregion setup
|
||||
providers: [ {provide: UserService, useValue: fakeUserService } ]
|
||||
providers: [ {provide: UserService, useValue: userServiceStub } ]
|
||||
});
|
||||
// #enddocregion config-test-module
|
||||
|
||||
|
@ -80,4 +85,8 @@ describe('WelcomeComponent', () => {
|
|||
expect(content).toMatch(/log in/i, '"log in"');
|
||||
});
|
||||
// #enddocregion tests
|
||||
|
||||
it('orig stub and injected UserService are not the same object', () => {
|
||||
expect(userServiceStub === userService).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
// export for convenience.
|
||||
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
|
||||
|
||||
import { Component, Directive, Injectable, Input } from '@angular/core';
|
||||
import { NavigationExtras } from '@angular/router';
|
||||
|
||||
@Directive({
|
||||
selector: '[routerLink]',
|
||||
host: {
|
||||
'(click)': 'onClick()',
|
||||
'[attr.href]': 'visibleHref',
|
||||
'[class.router-link-active]': 'isRouteActive'
|
||||
}
|
||||
})
|
||||
export class FakeRouterLinkDirective {
|
||||
|
||||
isRouteActive = false;
|
||||
visibleHref: string; // the url displayed on the anchor element.
|
||||
|
||||
@Input('routerLink') linkParams: any;
|
||||
navigatedTo: any = null;
|
||||
|
||||
onClick() {
|
||||
this.navigatedTo = this.linkParams;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'router-outlet', template: ''})
|
||||
export class FakeRouterOutletComponent { }
|
||||
|
||||
@Injectable()
|
||||
export class FakeRouter {
|
||||
lastCommand: any[];
|
||||
navigate(commands: any[], extras?: NavigationExtras) {
|
||||
this.lastCommand = commands;
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FakeActivatedRoute {
|
||||
testParams: {} = {};
|
||||
|
||||
get snapshot() {
|
||||
return {
|
||||
params: this.testParams
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { tick, ComponentFixture } from '@angular/core/testing';
|
||||
|
||||
export * from './jasmine-matchers';
|
||||
export * from './fake-router';
|
||||
export * from './router-stubs';
|
||||
|
||||
// Short utilities
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// export for convenience.
|
||||
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
|
||||
|
||||
import { Component, Directive, Injectable, Input } from '@angular/core';
|
||||
import { NavigationExtras } from '@angular/router';
|
||||
|
||||
@Directive({
|
||||
selector: '[routerLink]',
|
||||
host: {
|
||||
'(click)': 'onClick()',
|
||||
'[attr.href]': 'href',
|
||||
'[class.router-link-active]': 'isRouteActive'
|
||||
}
|
||||
})
|
||||
export class RouterLinkDirectiveStub {
|
||||
|
||||
isRouteActive = false;
|
||||
href: string; // the url displayed on the anchor element.
|
||||
|
||||
@Input('routerLink') linkParams: any;
|
||||
navigatedTo: any = null;
|
||||
|
||||
onClick() {
|
||||
this.navigatedTo = this.linkParams;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'router-outlet', template: ''})
|
||||
export class RouterOutletStubComponent { }
|
||||
|
||||
@Injectable()
|
||||
export class RouterStub {
|
||||
lastCommand: any[];
|
||||
navigate(commands: any[], extras?: NavigationExtras) {
|
||||
this.lastCommand = commands;
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Only implements params and part of snapshot.params
|
||||
// #docregion activated-route-stub
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
@Injectable()
|
||||
export class ActivatedRouteStub {
|
||||
|
||||
// ActivatedRoute.params is Observable
|
||||
private subject = new BehaviorSubject(this.testParams);
|
||||
params = this.subject.asObservable();
|
||||
|
||||
// Test parameters
|
||||
private _testParams: {};
|
||||
get testParams() { return this._testParams; }
|
||||
set testParams(params: {}) {
|
||||
this._testParams = params;
|
||||
this.subject.next(params);
|
||||
}
|
||||
|
||||
// ActivatedRoute.snapshot.params
|
||||
get snapshot() {
|
||||
return { params: this.testParams };
|
||||
}
|
||||
}
|
||||
// #enddocregion activated-route-stub
|
|
@ -27,6 +27,8 @@ a#top
|
|||
* [Test a component with inputs and outputs](#component-with-inputs-output)
|
||||
* [Test a component inside a test host component](#component-inside-test-host)
|
||||
* [Test a routed component](#routed-component)
|
||||
* [Test a routed component with parameters](#routed-component-w-param)
|
||||
* [Use a _page_ object to simplify setup](#page-object)
|
||||
* [Isolated tests](#testing-without-atp "Testing without the Angular Testing Platform")
|
||||
* [_TestBed_ API](#atp-api)
|
||||
* [FAQ](#faq "Frequently asked questions")
|
||||
|
@ -177,9 +179,9 @@ table(width="100%")
|
|||
td
|
||||
:marked
|
||||
[SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md)
|
||||
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.
|
||||
loads the application and test files.
|
||||
This script tells SystemJS where to find those files and how to load them.
|
||||
It's the same version of `systemjs.config.js` used by QuickStart-based applications.
|
||||
tr
|
||||
td(style="vertical-align: top") <code>systemjs.config.extras.js</code>
|
||||
td
|
||||
|
@ -344,7 +346,7 @@ a#atp-intro
|
|||
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
|
||||
The tester creates a test instance of the class with new, supplying test doubles for the constructor parameters as needed, and
|
||||
then probes the test instance API surface.
|
||||
|
||||
Isolated tests don't reveal how the class interacts with Angular.
|
||||
|
@ -355,12 +357,8 @@ a#atp-intro
|
|||
### Testing with the _ Angular Testing Platform_
|
||||
|
||||
The _Angular Testing Platform_ consists of the `TestBed` class and some helper functions from `@angular/core/testing`.
|
||||
.alert.is-important
|
||||
:marked
|
||||
The _TestBed_ is officially _experimental_ and thus subject to change.
|
||||
Consult the [API reference](../api/core/testing/index/TestBed-class.html) for the latest status.
|
||||
:marked
|
||||
The `TestBed` creates an Angular test module — an `@NgModule` class —
|
||||
|
||||
The `TestBed` creates an Angular testing 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.
|
||||
|
||||
|
@ -372,19 +370,20 @@ a#atp-intro
|
|||
:marked
|
||||
You can access that hidden instance anytime by calling `getTestBed()`;
|
||||
:marked
|
||||
This `TestBed` instance comes pre-configured with a baseline of default providers and declarables (components, directives, and pipes)
|
||||
Thanks to initialization in the [testing shims](#setup),
|
||||
the default `TestBed` instance is 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`
|
||||
|
||||
The shims in this chapter are designed for testing a browser application so the default configuration 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.
|
||||
This default testing module configuration is a _foundation_ for testing _any_ browser app.
|
||||
You call `TestBed.configureTestingModule` with an object that defines additional imports, declarations, providers and schemas
|
||||
to reshape the testing module to fit your application tests.
|
||||
Optional `override...` methods can fine-tune aspects of the configuration.
|
||||
|
||||
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.
|
||||
that you'll need to inspect and control the component's immediate environment.
|
||||
|
||||
+makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-before-each', 'app/banner.component.spec.ts (simplified)')(format='.')
|
||||
:marked
|
||||
|
@ -393,7 +392,7 @@ a#atp-intro
|
|||
and see the effects of these actions both in the test component and in the test DOM.
|
||||
+makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-it', 'app/banner.component.spec.ts (simplified)')(format='.')
|
||||
:marked
|
||||
A comprehensive review of the _TestBed_ API appears [later in the chapter](#atp-api).
|
||||
A comprehensive review of the _Angular Testing Platform_ APIs appears [later in the chapter](#atp-api).
|
||||
Let's dive right into Angular testing, starting with with the components of a sample application.
|
||||
|
||||
a(href="#top").to-top Back to top
|
||||
|
@ -415,7 +414,7 @@ a#sample-app
|
|||
|
||||
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.
|
||||
It is large and can take up to a minute to start. Please be patient.
|
||||
<live-example plnkr="app-specs" embedded img="devguide/testing/app-specs-plunker.png"></live-example>
|
||||
|
||||
a(href="#top").to-top Back to top
|
||||
|
@ -444,7 +443,7 @@ a#simple-component-test
|
|||
`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
|
||||
It lacks `imports` because (a) it extends the default testing module configuration which
|
||||
already has what `BannerComponent` needs
|
||||
and (b) `BannerComponent` doesn't interact with any other components.
|
||||
|
||||
|
@ -539,18 +538,18 @@ a#component-with-dependency
|
|||
:marked
|
||||
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`:
|
||||
Here's the testing module configuration for the spec file, `app/welcome.component.spec.ts`:
|
||||
+makeExample('testing/ts/app/welcome.component.spec.ts', 'config-test-module', 'app/welcome.component.spec.ts')(format='.')
|
||||
:marked
|
||||
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`.
|
||||
This example configures the testing module with a stub `UserService`.
|
||||
|
||||
## Provide service fakes
|
||||
## Provide service test doubles
|
||||
|
||||
A component under test doesn't have to be injected with real services.
|
||||
In fact, it is usually better if they are fakes.
|
||||
In fact, it is usually better if they are test doubles (stubs, fakes, spies, or mocks).
|
||||
The purpose of the spec is to test the component, not the service,
|
||||
and real services can be trouble.
|
||||
|
||||
|
@ -558,22 +557,21 @@ a#component-with-dependency
|
|||
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`.
|
||||
It is far easier to create and register a test double in place of the real `UserService`.
|
||||
|
||||
There are many ways to fake a service.
|
||||
This test suit supplies a minimal `UserService` that satisfies the needs of the `WelcomeComponent`
|
||||
This particular test suite supplies a minimal `UserService` stub that satisfies the needs of the `WelcomeComponent`
|
||||
and its tests:
|
||||
+makeExample('testing/ts/app/welcome.component.spec.ts', 'fake-userservice')(format='.')
|
||||
+makeExample('testing/ts/app/welcome.component.spec.ts', 'user-service-stub')(format='.')
|
||||
|
||||
a#injected-service-reference
|
||||
:marked
|
||||
## Referencing injected services
|
||||
The tests need access to the injected (fake) `UserService`.
|
||||
The tests need access to the injected (stubbed) `UserService`.
|
||||
|
||||
You cannot reference the `fakeUserService` object provided to the test module.
|
||||
You cannot reference the `userServiceStub` object provided to the testing module.
|
||||
**It does not work!**
|
||||
Surprisingly, the instance actually injected into the component is _not the same_
|
||||
as the provided `fakeUserService` object.
|
||||
Surprisingly, the instance actually injected into the component is _not the same_ object
|
||||
as the provided `userServiceStub`.
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
|
@ -609,7 +607,7 @@ a#welcome-spec-setup
|
|||
And here are some tests:
|
||||
+makeExample('testing/ts/app/welcome.component.spec.ts', 'tests', 'app/welcome.component.spec.ts')(format='.')
|
||||
:marked
|
||||
The first is a sanity test; it confirms that the fake `UserService` is working.
|
||||
The first is a sanity test; it confirms that the stubbed `UserService` is called and 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.
|
||||
|
@ -634,7 +632,7 @@ a#component-with-async-service
|
|||
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:
|
||||
They should emulate such calls. The setup in this `app/shared/twain.component.spec.ts` shows one way to do that:
|
||||
+makeExample('testing/ts/app/shared/twain.component.spec.ts', 'setup', 'app/shared/twain.component.spec.ts (setup)')(format='.')
|
||||
|
||||
a#service-spy
|
||||
|
@ -642,7 +640,7 @@ a#service-spy
|
|||
### Spying on the real service
|
||||
|
||||
This setup is similar to the [`welcome.component.spec` setup](#welcome-spec-setup).
|
||||
But instead of creating a fake service object, it injects the _real_ service (see the test module `providers`) and
|
||||
But instead of creating a stubbed service object, it injects the _real_ service (see the testing module `providers`) and
|
||||
replaces the critical `getQuote` method with a Jasmine spy.
|
||||
+makeExample('testing/ts/app/shared/twain.component.spec.ts', 'spy')(format='.')
|
||||
:marked
|
||||
|
@ -669,24 +667,27 @@ a#service-spy
|
|||
|
||||
The test must become an "async test" ... like the third test
|
||||
|
||||
a#async-fn-in-it
|
||||
a#async
|
||||
:marked
|
||||
## The _async_ function in _it_
|
||||
|
||||
Notice the `async` in the third test.
|
||||
+makeExample('testing/ts/app/shared/twain.component.spec.ts', 'async-test', 'app/shared/twain.component.spec.ts (async test)')(format='.')
|
||||
:marked
|
||||
The `async` function is part of the _Angular TestBed_ feature set.
|
||||
It _takes_ a parameterless function and _returns_ a parameterless function
|
||||
The `async` function is an independent feature of the _Angular Testing Platform_.
|
||||
|
||||
It simplifyies coding of asynchronous tests by arranging for the tester's code to run in a special _async test zone_.
|
||||
|
||||
The `async` function _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.
|
||||
There is nothing obviously asynchronous about it.
|
||||
For example, it doesn't return a promise and
|
||||
there is no `done` function to call as there is in standard Jasmine asynchronous tests.
|
||||
|
||||
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.
|
||||
Some functions called within a test (such as `fixture.whenStable`) continue to reveal their asynchronous behavior.
|
||||
Consider also the [_fakeAsync_](#fake-async) alternative which affords a more linear coding experience.
|
||||
|
||||
a#when-stable
|
||||
:marked
|
||||
|
@ -720,20 +721,18 @@ a#fake-async
|
|||
+makeExample('testing/ts/app/shared/twain.component.spec.ts', 'fake-async-test', 'app/shared/twain.component.spec.ts (fakeAsync test)')(format='.')
|
||||
:marked
|
||||
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
|
||||
The `fakeAsync` function is another, independent feature of the _Angular Testing Platform_.
|
||||
|
||||
Like [async](#), it _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 `fakeAsync` function enables a linear coding style by running the test body in a special _fakeAsync test zone_.
|
||||
|
||||
The key advantage of `fakeAsync` is that the test body looks entirely synchronous.
|
||||
The principle advantage of `fakeAsync` over `async` is that the test appears to be synchronous.
|
||||
There are no promises at all.
|
||||
No `then(...)` chains to disrupt the visible flow of control.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
There are limitations. For example, you cannot make an XHR call from within a `fakeAsync`.
|
||||
:marked
|
||||
|
||||
a#tick
|
||||
a#tick-first-look
|
||||
|
@ -741,7 +740,7 @@ a#tick-first-look
|
|||
## 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`.
|
||||
The `tick` function is a part of the _Angular Testing Platform_ 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,
|
||||
|
@ -796,7 +795,7 @@ a#component-with-external-template
|
|||
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"
|
||||
current testing module. After it completes, external templates and css files, have been "inlined"
|
||||
and `TestBed.createComponent` can do its job synchronously.
|
||||
.l-sub-section
|
||||
:marked
|
||||
|
@ -811,22 +810,14 @@ a#async-fn-in-before-each
|
|||
## 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.
|
||||
that hides the mechanics of asynchronous execution, just as it does when passed to an [_it_ test)(#async).
|
||||
|
||||
a#compile-components
|
||||
:marked
|
||||
## _compileComponents_
|
||||
In this example, `Testbed.compileComponents` compiles one component, the `DashboardComponent`.
|
||||
It's the only declared component in this test module.
|
||||
It's the only declared component in this testing module.
|
||||
|
||||
Tests later in this chapter have more declared components and some of them import application
|
||||
modules that declare yet more components.
|
||||
|
@ -884,8 +875,12 @@ a#component-with-inputs-outputs
|
|||
+makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.')
|
||||
:marked
|
||||
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).
|
||||
|
||||
You'd probably have to replace them both with test doubles and that looks like a lot of work.
|
||||
The router seems particularly challenging.
|
||||
.l-sub-section
|
||||
:marked
|
||||
The [discussion below](#routed-component) covers testing components that requre the router.
|
||||
:marked
|
||||
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.
|
||||
|
||||
|
@ -957,7 +952,7 @@ a#component-inside-test-host
|
|||
The setup for the test-host tests is similar to the setup for the stand-alone tests:
|
||||
+makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-setup', 'app/dashboard/dashboard-hero.component.spec.ts (test host setup)')(format='.')
|
||||
:marked
|
||||
This test module configuration shows two important differences:
|
||||
This testing module configuration shows two important differences:
|
||||
1. It _declares_ both the `DashboardHeroComponent` and the `TestHostComponent`.
|
||||
1. It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent`.
|
||||
|
||||
|
@ -994,10 +989,10 @@ a#routed-component
|
|||
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:
|
||||
+makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'fake-router', 'app/dashboard/dashboard.component.spec.ts (fakeRouter)')(format='.')
|
||||
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='.')
|
||||
:marked
|
||||
Now we setup the test module with the `fakeRouter` and a fake `HeroService` and
|
||||
Now we setup the testing module with test stubs for the `Router` and `HeroService` and
|
||||
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='.')
|
||||
:marked
|
||||
|
@ -1011,7 +1006,7 @@ a#inject
|
|||
Notice the `inject` function in the second `it` argument.
|
||||
+makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'inject')(format='.')
|
||||
:marked
|
||||
The `inject` function is part of the _Angular TestBed_ feature set.
|
||||
The `inject` function is an independent feature of the _Angular Testing Platform_.
|
||||
It injects services into the test function where you can alter, spy on, and manipulate them.
|
||||
|
||||
The `inject` function has two parameters
|
||||
|
@ -1046,6 +1041,138 @@ a(href="#top").to-top Back to top
|
|||
|
||||
.l-hr
|
||||
|
||||
a#routed-component-w-param
|
||||
:marked
|
||||
# Test a routed component with parameters
|
||||
|
||||
Clicking a _Dashboard_ hero triggers navigation to `heroes/:id` where `:id`
|
||||
is a route parameter whose value is the `id` of the hero to edit.
|
||||
That URL matches a route to the `HeroDetailComponent`.
|
||||
|
||||
The router pushes the `:id` token value into the `ActivatedRoute.params` _Observable_ property,
|
||||
Angular injects the `ActivatedRoute` into the `HeroDetailComponent`,
|
||||
and the component extracts the `id` so it can fetch the corresponding hero via the `HeroDetailService`.
|
||||
Here's the `HeroDetailComponent` constructor:
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.ts', 'ctor', 'app/hero/hero-detail.component.ts (constructor)')(format='.')
|
||||
:marked
|
||||
`HeroDetailComponent` listens for changes to the `ActivatedRoute.params` in its `ngOnInit` method.
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.ts', 'ng-on-init', 'app/hero/hero-detail.component.ts (ngOnInit)')(format='.')
|
||||
.l-sub-section
|
||||
:marked
|
||||
The expression after `route.params` chains an _Observable_ operator that _plucks_ the `id` from the `params`
|
||||
and then chains a `forEach` operator to subscribes to `id`-changing events.
|
||||
The `id` changes every time the user navigates to a different hero.
|
||||
|
||||
The `forEach` passes the new `id` value to the component's `getHero` method (not shown)
|
||||
which fetches a hero and sets the component's `hero` property.
|
||||
If the`id` parameter is missing, the `pluck` operator fails and the `catch` treats failure as a request to edit a new hero.
|
||||
|
||||
The [Router](router.html#route-parameters) chapter covers `ActivatedRoute.params` in more detail.
|
||||
:marked
|
||||
A test can explore how the `HeroDetailComponent` responds to different `id` parameter values
|
||||
by manipulating the `ActivatedRoute` injected into the component's constructor.
|
||||
|
||||
By now you know how to stub the `Router` and a data service.
|
||||
Stubbing the `ActivatedRoute` would follow the same pattern except for a complication:
|
||||
the `ActivatedRoute.params` is an _Observable_.
|
||||
a#stub-observable
|
||||
:marked
|
||||
### _Observable_ test double
|
||||
|
||||
The `hero-detail.component.spec.ts` relies on an `ActivatedRouteStub` to set `ActivatedRoute.params` values for each test.
|
||||
This is a cross-application, re-usable _test helper class_.
|
||||
We recommend locating such helpers in a `testing` folder sibling to the `app` folder.
|
||||
This sample keeps `ActivatedRouteStub` in `testing/router-stubs.ts`:
|
||||
|
||||
+makeExample('testing/ts/testing/router-stubs.ts', 'activated-route-stub', 'testing/router-stubs.ts (ActivatedRouteStub)')(format='.')
|
||||
:marked
|
||||
Notable features of this stub:
|
||||
|
||||
* The stub implements only two of the `ActivatedRoute` capabilities: `params` and `snapshot.params`.
|
||||
|
||||
* <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md" target="_blank">_BehaviorSubject_</a>
|
||||
drives the stub's `params` _Observable_ and returns the same value to every `params` subscriber until it's given a new value.
|
||||
|
||||
* The `HeroDetailComponent` chain its expressions to this stub `params` _Observable_ which is now under the tester's control.
|
||||
|
||||
* Setting the `testParams` property causes the `subject` to push the assigned value into `params`.
|
||||
That triggers the `HeroDetailComponent` _params_ subscription, described above, in the same way that navigation does.
|
||||
|
||||
* Setting the `testParams` property also updates the stub's internal value for the `snapshot` property to return.
|
||||
.l-sub-section(style="margin-left:30px")
|
||||
:marked
|
||||
The [_snapshot_](router.html#snapshot "Router Chapter: snapshot") is another popular way for components to consume route parameters.
|
||||
.callout.is-helpful
|
||||
:marked
|
||||
The router stubs in this chapter are meant to inspire you. Create your own stubs to fit your testing needs.
|
||||
|
||||
a#observable-tests
|
||||
:marked
|
||||
### _Observable_ tests
|
||||
Here's a test demonstrating the component's behavior when the observed `id` refers to an existing hero:
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-good-id', 'app/hero/hero-detail.component.spec.ts (existing id)')(format='.')
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `createComponent` method and `page` object are discussed [in the next section](#page-object).
|
||||
Rely on your intuition for now.
|
||||
:marked
|
||||
When the `id` cannot be found, the component should re-route to the `HeroListComponent`.
|
||||
The test suite setup provided the same `RouterStub` [described above](#routed-component) which spies on the router without actually navigating.
|
||||
This test supplies a "bad" id and expects the component to try to navigate.
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-bad-id', 'app/hero/hero-detail.component.spec.ts (bad id)')(format='.')
|
||||
:marked
|
||||
:marked
|
||||
While this app doesn't have a route to the `HeroDetailComponent` that omits the `id` parameter, it might add such a route someday.
|
||||
The component should do something reasonable when there is no `id`.
|
||||
|
||||
In this implementation, the component should create and display a new hero.
|
||||
New heroes have `id=0` and a blank `name`. This test confirms that the component behaves as expected:
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-no-id', 'app/hero/hero-detail.component.spec.ts (no id)')(format='.')
|
||||
:marked
|
||||
.callout.is-helpful
|
||||
:marked
|
||||
Inspect and download _all_ of the chapter's application test code with this <live-example plnkr="app-specs">live example</live-example>.
|
||||
|
||||
.l-hr
|
||||
|
||||
a#page-object
|
||||
:marked
|
||||
# Use a _page_ object to simplify setup
|
||||
|
||||
The `HeroDetailComponent` is a simple view with a title, two hero fields, and two buttons.
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/testing/hero-detail.component.png' alt="HeroDetailComponent in action")
|
||||
:marked
|
||||
But there's already plenty of template complexity.
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.html', '', 'app/hero/hero-detail.component.html')(format='.')
|
||||
:marked
|
||||
To fully exercise the component, the test needs ...
|
||||
* to wait until a `hero` arrives before `*ngIf` allows any element in DOM
|
||||
* element references for the title name span and name input-box to inspect their values
|
||||
* two button references to click
|
||||
* spies on services and component methods
|
||||
|
||||
Even a small form such as this one can produce a mess of tortured conditional setup and CSS element selection.
|
||||
|
||||
Tame the madness with a `Page` class that simplifies access to component properties and encapsulates the logic that sets them.
|
||||
Here's the `Page` class for the `hero-detail.component.spec.ts`
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'page', 'app/hero/hero-detail.component.spec.ts (Page)')(format='.')
|
||||
:marked
|
||||
Now the important hooks for component manipulation and inspection are neatly organized and accessible from an instance of `Page`.
|
||||
|
||||
A `createComponent` method creates a `page` and fills in the blanks once the `hero` arrives.
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'create-component', 'app/hero/hero-detail.component.spec.ts (createComponent)')(format='.')
|
||||
:marked
|
||||
The [observable tests](#observable-tests) in the previous section demonstrate how `createComponent` and `page`
|
||||
keep the tests short and _on message_.
|
||||
There are no distractions: no waiting for promises to resolve and no searching the DOM for element values to compare.
|
||||
Here are a few more `HeroDetailComponent` tests to drive the point home.
|
||||
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'selected-tests', 'app/hero/hero-detail.component.spec.ts (selected tests)')(format='.')
|
||||
:marked
|
||||
|
||||
a(href="#top").to-top Back to top
|
||||
.l-hr
|
||||
|
||||
a#isolated-tests
|
||||
a#testing-without-atp
|
||||
:marked
|
||||
|
@ -1067,7 +1194,7 @@ a#testing-without-atp
|
|||
They do
|
||||
* exhibit standard, Angular-agnostic testing techniques
|
||||
* create instances directly with `new`
|
||||
* use stubs, spys, and mocks to fake dependencies.
|
||||
* substitute test doubles (stubs, spys, and mocks) for the real dependencies.
|
||||
|
||||
.callout.is-important
|
||||
header Write both kinds of tests
|
||||
|
@ -1119,7 +1246,7 @@ a#testing-without-atp
|
|||
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
|
||||
You can mock the dependency, or use a dummy 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
|
||||
|
@ -1143,7 +1270,7 @@ a#testing-without-atp
|
|||
Use simple Jasmine to explore the expected cases and the edge cases.
|
||||
+makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'excerpt', 'app/shared/title-case.pipe.spec.ts')
|
||||
:marked
|
||||
## Write ATP tests too
|
||||
### 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.
|
||||
|
@ -1208,24 +1335,30 @@ table
|
|||
td
|
||||
:marked
|
||||
Runs the body of a test (`it`) or setup (`beforeEach`) function within a special _async test zone_.
|
||||
See [here](#async-fn-in-it) and [here](#async-fn-in-before-each).
|
||||
See [discussion above](#async).
|
||||
tr
|
||||
td(style="vertical-align: top") <code>fakeAsync</code>
|
||||
td
|
||||
:marked
|
||||
Runs the body of a test (`it`) within a special _fakeAsync test zone_, enabling
|
||||
a linear control flow coding style. See [above](#fake-async).
|
||||
a linear control flow coding style. See [discussion above](#fake-async).
|
||||
tr
|
||||
td(style="vertical-align: top") <code>tick</code>
|
||||
td
|
||||
:marked
|
||||
Simulates the passage of time and the completion of pending asynchronous activities
|
||||
by flushing timer and micro-task queues in the _fakeAsync test zone_.
|
||||
by flushing both _timer_ and _micro-task_ queues within the _fakeAsync test zone_.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The curious, dedicated reader might enjoy this lengthy blog post,
|
||||
"<a href="https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/"
|
||||
target="_blank">_Tasks, microtasks, queues and schedules_</a>".
|
||||
:marked
|
||||
Accepts an optional argument that moves the virtual clock forward
|
||||
the specified number of milliseconds,
|
||||
clearing asynchronous activities scheduled within that timeframe.
|
||||
See [above](#tick).
|
||||
See [discussion bove](#tick).
|
||||
|
||||
tr
|
||||
td(style="vertical-align: top") <code>inject</code>
|
||||
|
@ -1238,22 +1371,22 @@ table
|
|||
td(style="vertical-align: top") <code>discardPeriodicTasks</code>
|
||||
td
|
||||
:marked
|
||||
When a `fakeAsync` test ends with pending timer event tasks (queued `setTimeOut` and `setInterval` callbacks),
|
||||
When a `fakeAsync` test ends with pending timer event _tasks_ (queued `setTimeOut` and `setInterval` callbacks),
|
||||
the test fails with a clear error message.
|
||||
|
||||
In general, a test should end with no queued tasks.
|
||||
When pending timer tasks are expected, call `discardPeriodicTasks` to flush the queues
|
||||
When pending timer tasks are expected, call `discardPeriodicTasks` to flush the _task_ queue
|
||||
and avoid the error.
|
||||
|
||||
tr
|
||||
td(style="vertical-align: top") <code>flushMicrotasks</code>
|
||||
td
|
||||
:marked
|
||||
When a `fakeAsync` test ends with pending "microtasks" such as unresolved promises,
|
||||
When a `fakeAsync` test ends with pending _micro-tasks_ such as unresolved promises,
|
||||
the test fails with a clear error message.
|
||||
|
||||
In general, a test should wait for microtasks to finish.
|
||||
When pending microtasks are expected, call `discardPeriodicTasks` to flush the queues
|
||||
In general, a test should wait for micro-tasks to finish.
|
||||
When pending microtasks are expected, call `flushMicrotasks` to flush the _micro-task_ queue
|
||||
and avoid the error.
|
||||
|
||||
tr
|
||||
|
@ -1277,16 +1410,11 @@ table
|
|||
a#testbed-class-summary
|
||||
:marked
|
||||
# _TestBed_ Class Summary
|
||||
The `TestBed` class API is quite large and can be overwhelming until you've explored it first
|
||||
The `TestBed` class is a principle feature of the _Angular Testing Platform_.
|
||||
Its 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.
|
||||
|
||||
.alert.is-important
|
||||
:marked
|
||||
The _TestBed_ is officially _experimental_ and thus subject to change.
|
||||
Consult the [API reference](../api/core/testing/index/TestBed-class.html) for the latest status.
|
||||
|
||||
:marked
|
||||
The module definition passed to `configureTestingModule`,
|
||||
is a subset of the `@NgModule` metadata properties.
|
||||
code-example(format="." language="javascript").
|
||||
|
@ -1327,18 +1455,18 @@ table
|
|||
td
|
||||
:marked
|
||||
The testing shims (`karma-test-shim`, `browser-test-shim`)
|
||||
establish the [initial test environment](#a#testbed-initTestEnvironment) and a default test module.
|
||||
The default test module is configured with basic declaratives and some Angular service substitutes (e.g. `DebugDomRender`)
|
||||
establish the [initial test environment](#a#testbed-initTestEnvironment) and a default testing module.
|
||||
The default testing module is configured with basic declaratives and some Angular service substitutes (e.g. `DebugDomRender`)
|
||||
that every tester needs.
|
||||
|
||||
Call `configureTestingModule` to refine the test module configuration for a particular set of tests
|
||||
Call `configureTestingModule` to refine the testing module configuration for a particular set of tests
|
||||
by adding and removing imports, declarations (of components, directives, and pipes), and providers.
|
||||
tr
|
||||
td(style="vertical-align: top") <code>compileComponents</code>
|
||||
td
|
||||
:marked
|
||||
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 `templateUrl`
|
||||
Compile the testing module asynchronously after you've finished configuring it.
|
||||
You **must** call this method if _any_ of the testing module components have a `templateUrl`
|
||||
or `styleUrls` because fetching component template and style files is necessarily asynchronous.
|
||||
See [above](#compile-components).
|
||||
|
||||
|
@ -1355,7 +1483,7 @@ table
|
|||
td
|
||||
:marked
|
||||
Replace metadata for the given `NgModule`. Recall that modules can import other modules.
|
||||
The `overrideModule` method can reach deeply into the current test module to
|
||||
The `overrideModule` method can reach deeply into the current testing module to
|
||||
modify one of these inner modules.
|
||||
tr
|
||||
td(style="vertical-align: top") <code>overrideComponent</code>
|
||||
|
@ -1408,14 +1536,14 @@ table
|
|||
This method may be called _exactly once_. Call `resetTestEnvironment` first
|
||||
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 test module.
|
||||
Test modules and platforms for individual platforms are available from
|
||||
Specify the Angular compiler factory, a `PlatformRef`, and a default Angular testing module.
|
||||
Alternatives for non-browser platforms are available from
|
||||
`angular2/platform/testing/<platform_name>`.
|
||||
tr
|
||||
td(style="vertical-align: top") <code>resetTestEnvironment</code>
|
||||
td
|
||||
:marked
|
||||
Reset the initial test environment including the default test module.
|
||||
Reset the initial test environment including the default testing module.
|
||||
|
||||
:marked
|
||||
A few of the `TestBed` instance methods are not covered by static `TestBed` _class_ methods.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Loading…
Reference in New Issue