docs(testing): import test module and override component providers (#2428)
This commit is contained in:
parent
36a3ea2c21
commit
4c71a32e36
|
@ -7,16 +7,13 @@ import { By } from '@angular/platform-browser';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addMatchers, newEvent,
|
ActivatedRoute, ActivatedRouteStub, newEvent, Router, RouterStub
|
||||||
ActivatedRoute, ActivatedRouteStub, Router, RouterStub
|
|
||||||
} from '../../testing';
|
} from '../../testing';
|
||||||
|
|
||||||
import { HEROES, FakeHeroService } from '../model/testing';
|
import { Hero } from '../model';
|
||||||
|
|
||||||
import { HeroModule } from './hero.module';
|
|
||||||
import { HeroDetailComponent } from './hero-detail.component';
|
import { HeroDetailComponent } from './hero-detail.component';
|
||||||
import { HeroDetailService } from './hero-detail.service';
|
import { HeroDetailService } from './hero-detail.service';
|
||||||
import { Hero, HeroService } from '../model';
|
import { HeroModule } from './hero.module';
|
||||||
|
|
||||||
////// Testing Vars //////
|
////// Testing Vars //////
|
||||||
let activatedRoute: ActivatedRouteStub;
|
let activatedRoute: ActivatedRouteStub;
|
||||||
|
@ -24,20 +21,117 @@ let comp: HeroDetailComponent;
|
||||||
let fixture: ComponentFixture<HeroDetailComponent>;
|
let fixture: ComponentFixture<HeroDetailComponent>;
|
||||||
let page: Page;
|
let page: Page;
|
||||||
|
|
||||||
////////// Tests ////////////////////
|
////// Tests //////
|
||||||
|
|
||||||
describe('HeroDetailComponent', () => {
|
describe('HeroDetailComponent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
activatedRoute = new ActivatedRouteStub();
|
||||||
|
});
|
||||||
|
describe('with HeroModule setup', heroModuleSetup);
|
||||||
|
describe('when override its provided HeroDetailService', overrideSetup);
|
||||||
|
describe('with FormsModule setup', formsModuleSetup);
|
||||||
|
describe('with SharedModule setup', sharedModuleSetup);
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
function overrideSetup() {
|
||||||
|
// #docregion stub-hds
|
||||||
|
class StubHeroDetailService {
|
||||||
|
testHero = new Hero(42, 'Test Hero');
|
||||||
|
|
||||||
|
getHero(id: number | string): Promise<Hero> {
|
||||||
|
return Promise.resolve(true).then(() => Object.assign({}, this.testHero) );
|
||||||
|
}
|
||||||
|
|
||||||
|
saveHero(hero: Hero): Promise<Hero> {
|
||||||
|
return Promise.resolve(true).then(() => Object.assign(this.testHero, hero) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #enddocregion stub-hds
|
||||||
|
|
||||||
|
// the `id` value is irrelevant because ignored by service stub
|
||||||
|
beforeEach(() => activatedRoute.testParams = { id: 99999 } );
|
||||||
|
|
||||||
|
// #docregion setup-override
|
||||||
|
beforeEach( async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ HeroModule ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: Router, useClass: RouterStub},
|
||||||
|
// #enddocregion setup-override
|
||||||
|
// HeroDetailService at this level is IRRELEVANT!
|
||||||
|
{ provide: HeroDetailService, useValue: {} }
|
||||||
|
// #docregion setup-override
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Override component's own provider
|
||||||
|
// #docregion override-component-method
|
||||||
|
.overrideComponent(HeroDetailComponent, {
|
||||||
|
set: {
|
||||||
|
providers: [
|
||||||
|
{ provide: HeroDetailService, useClass: StubHeroDetailService }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #enddocregion override-component-method
|
||||||
|
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
// #enddocregion setup-override
|
||||||
|
|
||||||
|
// #docregion override-tests
|
||||||
|
let hds: StubHeroDetailService;
|
||||||
|
|
||||||
beforeEach( async(() => {
|
beforeEach( async(() => {
|
||||||
addMatchers();
|
createComponent();
|
||||||
activatedRoute = new ActivatedRouteStub();
|
// get the component's injected StubHeroDetailService
|
||||||
|
hds = fixture.debugElement.injector.get(HeroDetailService);
|
||||||
|
}));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
it('should display stub hero\'s name', () => {
|
||||||
imports: [ HeroModule ],
|
expect(page.nameDisplay.textContent).toBe(hds.testHero.name);
|
||||||
|
});
|
||||||
|
|
||||||
// DON'T RE-DECLARE because already declared in HeroModule
|
it('should save stub hero change', fakeAsync(() => {
|
||||||
// declarations: [HeroDetailComponent, TitleCasePipe], // No!
|
const origName = hds.testHero.name;
|
||||||
|
const newName = 'New Name';
|
||||||
|
|
||||||
|
page.nameInput.value = newName;
|
||||||
|
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
|
||||||
|
|
||||||
|
expect(comp.hero.name).toBe(newName, 'component hero has new name');
|
||||||
|
expect(hds.testHero.name).toBe(origName, 'service hero unchanged before save');
|
||||||
|
|
||||||
|
page.saveBtn.triggerEventHandler('click', null);
|
||||||
|
tick(); // wait for async save to complete
|
||||||
|
expect(hds.testHero.name).toBe(newName, 'service hero has new name after save');
|
||||||
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||||
|
}));
|
||||||
|
// #enddocregion override-tests
|
||||||
|
|
||||||
|
it('fixture injected service is not the component injected service',
|
||||||
|
inject([HeroDetailService], (service: HeroDetailService) => {
|
||||||
|
|
||||||
|
expect(service).toEqual({}, 'service injected from fixture');
|
||||||
|
expect(hds).toBeTruthy('service injected into component');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
import { HEROES, FakeHeroService } from '../model/testing';
|
||||||
|
import { HeroService } from '../model';
|
||||||
|
|
||||||
|
const firstHero = HEROES[0];
|
||||||
|
|
||||||
|
function heroModuleSetup() {
|
||||||
|
// #docregion setup-hero-module
|
||||||
|
beforeEach( async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ HeroModule ],
|
||||||
|
// #enddocregion setup-hero-module
|
||||||
|
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
|
||||||
|
// #docregion setup-hero-module
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{ provide: HeroService, useClass: FakeHeroService },
|
{ provide: HeroService, useClass: FakeHeroService },
|
||||||
|
@ -46,13 +140,14 @@ describe('HeroDetailComponent', () => {
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
}));
|
}));
|
||||||
|
// #enddocregion setup-hero-module
|
||||||
|
|
||||||
// #docregion route-good-id
|
// #docregion route-good-id
|
||||||
describe('when navigate to hero id=' + HEROES[0].id, () => {
|
describe('when navigate to existing hero', () => {
|
||||||
let expectedHero: Hero;
|
let expectedHero: Hero;
|
||||||
|
|
||||||
beforeEach( async(() => {
|
beforeEach( async(() => {
|
||||||
expectedHero = HEROES[0];
|
expectedHero = firstHero;
|
||||||
activatedRoute.testParams = { id: expectedHero.id };
|
activatedRoute.testParams = { id: expectedHero.id };
|
||||||
createComponent();
|
createComponent();
|
||||||
}));
|
}));
|
||||||
|
@ -76,7 +171,7 @@ describe('HeroDetailComponent', () => {
|
||||||
|
|
||||||
it('should navigate when click save and save resolves', fakeAsync(() => {
|
it('should navigate when click save and save resolves', fakeAsync(() => {
|
||||||
page.saveBtn.triggerEventHandler('click', null);
|
page.saveBtn.triggerEventHandler('click', null);
|
||||||
tick(); // wait for async save to "complete" before navigating
|
tick(); // wait for async save to complete
|
||||||
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -91,8 +186,7 @@ describe('HeroDetailComponent', () => {
|
||||||
// dispatch a DOM event so that Angular learns of input value change.
|
// dispatch a DOM event so that Angular learns of input value change.
|
||||||
page.nameInput.dispatchEvent(newEvent('input'));
|
page.nameInput.dispatchEvent(newEvent('input'));
|
||||||
|
|
||||||
// detectChanges() makes [(ngModel)] push input value to component property
|
// Tell Angular to update the output span through the title pipe
|
||||||
// and Angular updates the output span through the title pipe
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(page.nameDisplay.textContent).toBe(titleCaseName);
|
expect(page.nameDisplay.textContent).toBe(titleCaseName);
|
||||||
|
@ -131,10 +225,8 @@ describe('HeroDetailComponent', () => {
|
||||||
});
|
});
|
||||||
// #enddocregion route-bad-id
|
// #enddocregion route-bad-id
|
||||||
|
|
||||||
///////////////////////////
|
|
||||||
|
|
||||||
// Why we must use `fixture.debugElement.injector` in `Page()`
|
// Why we must use `fixture.debugElement.injector` in `Page()`
|
||||||
it('cannot use `inject` to get component\'s provided service', () => {
|
it('cannot use `inject` to get component\'s provided HeroDetailService', () => {
|
||||||
let service: HeroDetailService;
|
let service: HeroDetailService;
|
||||||
fixture = TestBed.createComponent(HeroDetailComponent);
|
fixture = TestBed.createComponent(HeroDetailComponent);
|
||||||
expect(
|
expect(
|
||||||
|
@ -148,7 +240,64 @@ describe('HeroDetailComponent', () => {
|
||||||
service = fixture.debugElement.injector.get(HeroDetailService);
|
service = fixture.debugElement.injector.get(HeroDetailService);
|
||||||
expect(service).toBeDefined('debugElement.injector');
|
expect(service).toBeDefined('debugElement.injector');
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TitleCasePipe } from '../shared/title-case.pipe';
|
||||||
|
|
||||||
|
function formsModuleSetup() {
|
||||||
|
// #docregion setup-forms-module
|
||||||
|
beforeEach( async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ FormsModule ],
|
||||||
|
declarations: [ HeroDetailComponent, TitleCasePipe ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: HeroService, useClass: FakeHeroService },
|
||||||
|
{ provide: Router, useClass: RouterStub},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
// #enddocregion setup-forms-module
|
||||||
|
|
||||||
|
it('should display 1st hero\'s name', fakeAsync(() => {
|
||||||
|
const expectedHero = firstHero;
|
||||||
|
activatedRoute.testParams = { id: expectedHero.id };
|
||||||
|
createComponent().then(() => {
|
||||||
|
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
|
function sharedModuleSetup() {
|
||||||
|
// #docregion setup-shared-module
|
||||||
|
beforeEach( async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ SharedModule ],
|
||||||
|
declarations: [ HeroDetailComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: HeroService, useClass: FakeHeroService },
|
||||||
|
{ provide: Router, useClass: RouterStub},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
// #enddocregion setup-shared-module
|
||||||
|
|
||||||
|
it('should display 1st hero\'s name', fakeAsync(() => {
|
||||||
|
const expectedHero = firstHero;
|
||||||
|
activatedRoute.testParams = { id: expectedHero.id };
|
||||||
|
createComponent().then(() => {
|
||||||
|
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/////////// Helpers /////
|
/////////// Helpers /////
|
||||||
|
|
||||||
|
@ -185,9 +334,10 @@ class Page {
|
||||||
const compInjector = fixture.debugElement.injector;
|
const compInjector = fixture.debugElement.injector;
|
||||||
const hds = compInjector.get(HeroDetailService);
|
const hds = compInjector.get(HeroDetailService);
|
||||||
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.navSpy = spyOn(router, 'navigate');
|
this.navSpy = spyOn(router, 'navigate');
|
||||||
|
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add page elements after hero arrives */
|
/** Add page elements after hero arrives */
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* tslint:disable:member-ordering */
|
||||||
|
// #docplaster
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import 'rxjs/add/operator/pluck';
|
import 'rxjs/add/operator/pluck';
|
||||||
|
@ -5,22 +7,24 @@ import 'rxjs/add/operator/pluck';
|
||||||
import { Hero } from '../model';
|
import { Hero } from '../model';
|
||||||
import { HeroDetailService } from './hero-detail.service';
|
import { HeroDetailService } from './hero-detail.service';
|
||||||
|
|
||||||
|
// #docregion prototype
|
||||||
@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: ['app/hero/hero-detail.component.css'],
|
styleUrls: ['app/hero/hero-detail.component.css'],
|
||||||
providers: [ HeroDetailService ]
|
providers: [ HeroDetailService ]
|
||||||
})
|
})
|
||||||
export class HeroDetailComponent implements OnInit {
|
export class HeroDetailComponent implements OnInit {
|
||||||
@Input() hero: Hero;
|
|
||||||
|
|
||||||
// #docregion ctor
|
// #docregion ctor
|
||||||
constructor(
|
constructor(
|
||||||
private heroDetailService: HeroDetailService,
|
private heroDetailService: HeroDetailService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router) {
|
private router: Router) {
|
||||||
}
|
}
|
||||||
// #enddocregion ctor
|
// #enddocregion ctor
|
||||||
|
// #enddocregion prototype
|
||||||
|
|
||||||
|
@Input() hero: Hero;
|
||||||
|
|
||||||
// #docregion ng-on-init
|
// #docregion ng-on-init
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -50,4 +54,6 @@ export class HeroDetailComponent implements OnInit {
|
||||||
gotoList() {
|
gotoList() {
|
||||||
this.router.navigate(['../'], {relativeTo: this.route});
|
this.router.navigate(['../'], {relativeTo: this.route});
|
||||||
}
|
}
|
||||||
|
// #docregion prototype
|
||||||
}
|
}
|
||||||
|
// #enddocregion prototype
|
||||||
|
|
|
@ -2,10 +2,13 @@ import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Hero, HeroService } from '../model';
|
import { Hero, HeroService } from '../model';
|
||||||
|
|
||||||
|
// #docregion prototype
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HeroDetailService {
|
export class HeroDetailService {
|
||||||
constructor(private heroService: HeroService) { }
|
constructor(private heroService: HeroService) { }
|
||||||
|
// #enddocregion prototype
|
||||||
|
|
||||||
|
// Returns a clone which caller may modify safely
|
||||||
getHero(id: number | string): Promise<Hero> {
|
getHero(id: number | string): Promise<Hero> {
|
||||||
if (typeof id === 'string') {
|
if (typeof id === 'string') {
|
||||||
id = parseInt(id as string, 10);
|
id = parseInt(id as string, 10);
|
||||||
|
@ -18,4 +21,6 @@ export class HeroDetailService {
|
||||||
saveHero(hero: Hero) {
|
saveHero(hero: Hero) {
|
||||||
return this.heroService.updateHero(hero);
|
return this.heroService.updateHero(hero);
|
||||||
}
|
}
|
||||||
|
// #docregion prototype
|
||||||
}
|
}
|
||||||
|
// #enddocregion prototype
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Hero } from './hero';
|
||||||
import { HEROES } from './test-heroes';
|
import { HEROES } from './test-heroes';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
/** Dummy HeroService that pretends to be real */
|
/** Dummy HeroService. Pretend it makes real http requests */
|
||||||
export class HeroService {
|
export class HeroService {
|
||||||
getHeroes() {
|
getHeroes() {
|
||||||
return Promise.resolve(HEROES);
|
return Promise.resolve(HEROES);
|
||||||
|
@ -21,9 +21,10 @@ export class HeroService {
|
||||||
|
|
||||||
updateHero(hero: Hero): Promise<Hero> {
|
updateHero(hero: Hero): Promise<Hero> {
|
||||||
return this.getHero(hero.id).then(h => {
|
return this.getHero(hero.id).then(h => {
|
||||||
return h ?
|
if (!h) {
|
||||||
Object.assign(h, hero) :
|
throw new Error(`Hero ${hero.id} not found`);
|
||||||
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
|
}
|
||||||
|
return Object.assign(h, hero);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
|
||||||
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
|
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
|
||||||
// Error.stackTraceLimit = Infinity; //
|
// Error.stackTraceLimit = Infinity; //
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
|
||||||
|
|
||||||
var baseURL = document.baseURI;
|
var baseURL = document.baseURI;
|
||||||
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
|
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
|
||||||
|
|
|
@ -52,6 +52,10 @@ block includes
|
||||||
- [_Observable_ test double](#stub-observable)
|
- [_Observable_ test double](#stub-observable)
|
||||||
1. [Use a _page_ object to simplify setup](#page-object)
|
1. [Use a _page_ object to simplify setup](#page-object)
|
||||||
<br><br>
|
<br><br>
|
||||||
|
1. [Setup with module imports](#import-module)
|
||||||
|
<br><br>
|
||||||
|
1. [Override component providers](#component-override)
|
||||||
|
<br><br>
|
||||||
1. [Test a _RouterOutlet_ component](#router-outlet-component)
|
1. [Test a _RouterOutlet_ component](#router-outlet-component)
|
||||||
- [stubbing unneeded components](#stub-component)
|
- [stubbing unneeded components](#stub-component)
|
||||||
- [Stubbing the _RouterLink_](#router-link-stub)
|
- [Stubbing the _RouterLink_](#router-link-stub)
|
||||||
|
@ -446,6 +450,7 @@ a(href="#top").to-top Back to top
|
||||||
This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial).
|
This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial).
|
||||||
|
|
||||||
The following live example shows how it works and provides the complete source code.
|
The following live example shows how it works and provides the complete source code.
|
||||||
|
Give it some time to load and warm up.
|
||||||
<live-example embedded img="devguide/testing/app-plunker.png"></live-example>
|
<live-example embedded img="devguide/testing/app-plunker.png"></live-example>
|
||||||
<br><br>
|
<br><br>
|
||||||
:marked
|
:marked
|
||||||
|
@ -1210,7 +1215,151 @@ figure.image-display
|
||||||
There are no distractions: no waiting for promises to resolve and no searching the DOM for element values to compare.
|
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.
|
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='.')
|
||||||
|
|
||||||
|
a(href="#top").to-top Back to top
|
||||||
|
.l-hr
|
||||||
|
|
||||||
|
#import-module
|
||||||
:marked
|
:marked
|
||||||
|
# Setup with module imports
|
||||||
|
Earlier component tests configured the testing module with a few `declarations` like this:
|
||||||
|
+makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (config)')(format='.')
|
||||||
|
:marked
|
||||||
|
The `DashboardComponent` is simple. It needs no help.
|
||||||
|
But more complex components often depend on other components, directives, pipes, and providers
|
||||||
|
and these must be added to the testing module too.
|
||||||
|
|
||||||
|
Fortunately, the `TestBed.configureTestingModule` parameter parallels
|
||||||
|
the metadata passed to the `@NgModule` decorator
|
||||||
|
which means you can also specify `providers` and `imports.
|
||||||
|
|
||||||
|
The `HeroDetailComponent` requires a lot of help despite its small size and simple construction.
|
||||||
|
In addition to the support it receives from the default testing module `CommonModule`, it needs:
|
||||||
|
* `NgModel` and friends in the `FormsModule` enable two-way data binding
|
||||||
|
* The `TitleCasePipe` from the `shared` folder
|
||||||
|
* Router services (which these tests are stubbing)
|
||||||
|
* Hero data access services (also stubbed)
|
||||||
|
|
||||||
|
One approach is to configure the testing module from the individual pieces as in this example:
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-forms-module', 'app/hero/hero-detail.component.spec.ts (FormsModule setup)')(format='.')
|
||||||
|
:marked
|
||||||
|
Because many app components need the `FormsModule` and the `TitleCasePipe`, the developer created
|
||||||
|
a `SharedModule` to combine these and other frequently requested parts.
|
||||||
|
The test configuration can use the `SharedModule` too as seen in this alternative setup:
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-shared-module', 'app/hero/hero-detail.component.spec.ts (SharedModule setup)')(format='.')
|
||||||
|
:marked
|
||||||
|
It's a bit tighter and smaller, with fewer import statements (not shown).
|
||||||
|
|
||||||
|
#feature-module-import
|
||||||
|
:marked
|
||||||
|
### Import the feature module
|
||||||
|
The `HeroDetailComponent` is part of the `HeroModule` [Feature Module](ngmodule.html#feature-modules) that aggregates more of the interdependent pieces
|
||||||
|
including the `SharedModule`.
|
||||||
|
Try a test configuration that imports the `HeroModule` like this one:
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-hero-module', 'app/hero/hero-detail.component.spec.ts (HeroModule setup)')(format='.')
|
||||||
|
:marked
|
||||||
|
That's _really_ crisp. Only the _test doubles_ in the `providers` remain. Even the `HeroDetailComponent` declaration is gone.
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
In fact, if you try to declare it, Angular throws an error because
|
||||||
|
`HeroDetailComponent` is declared in both the `HeroModule` and the `DynamicTestModule` (the testing module).
|
||||||
|
|
||||||
|
.alert.is-helpful
|
||||||
|
:marked
|
||||||
|
Importing the component's feature module is often the easiest way to configure the tests,
|
||||||
|
especially when the feature module is small and mostly self-contained ... as feature modules should be.
|
||||||
|
:marked
|
||||||
|
|
||||||
|
a(href="#top").to-top Back to top
|
||||||
|
.l-hr
|
||||||
|
|
||||||
|
#component-override
|
||||||
|
:marked
|
||||||
|
# Override component providers
|
||||||
|
|
||||||
|
The `HeroDetailComponent` provides its own `HeroDetailService`.
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.ts', 'prototype', 'app/hero/hero-detail.component.ts (prototype)')(format='.')
|
||||||
|
:marked
|
||||||
|
It's not possible to stub the component's `HeroDetailService` in the `providers` of the `TestBed.configureTestingModule`.
|
||||||
|
Those are providers for the _testing module_, not the component. They prepare the dependency injector at the _fixture level_.
|
||||||
|
|
||||||
|
Angular creates the component with its _own_ injector which is a _child_ of the fixture injector.
|
||||||
|
It registers the component's providers (the `HeroDetailService` in this case) with the child injector.
|
||||||
|
A test cannot get to child injector services from the fixture injector.
|
||||||
|
And `TestBed.configureTestingModule` can't configure them either.
|
||||||
|
|
||||||
|
Angular has been creating new instances of the real `HeroDetailService` all along!
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
These tests could fail or timeout if the `HeroDetailService` made its own XHR calls to a remote server.
|
||||||
|
There might not be a remote server to call.
|
||||||
|
|
||||||
|
Fortunately, the `HeroDetailService` delegates responsibility for remote data access to an injected `HeroService`.
|
||||||
|
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.service.ts', 'prototype', 'app/hero/hero-detail.service.ts (prototype)')(format='.')
|
||||||
|
:marked
|
||||||
|
The [previous test configuration](#feature-module-import) replaces the real `HeroService` with a `FakeHeroService`
|
||||||
|
that intercepts server requests and fakes their responses.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
What if you aren't so lucky. What if faking the `HeroService` is hard?
|
||||||
|
What if `HeroDetailService` makes its own server requests?
|
||||||
|
|
||||||
|
The `TestBed.overrideComponent` method can replace the component's `providers` with easy-to-manage _test doubles_
|
||||||
|
as seen in the following setup variation:
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-override', 'app/hero/hero-detail.component.spec.ts (Override setup)')(format='.')
|
||||||
|
:marked
|
||||||
|
Notice that `TestBed.configureTestingModule` no longer provides a (fake) `HeroService` because it's [not needed](#stub-hero-detail-service).
|
||||||
|
|
||||||
|
#override-component-method
|
||||||
|
:marked
|
||||||
|
### The _overrideComponent_ method
|
||||||
|
|
||||||
|
Focus on the `overrideComponent` method.
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-component-method', 'app/hero/hero-detail.component.spec.ts (overrideComponent)')(format='.')
|
||||||
|
:marked
|
||||||
|
It takes two arguments: the component type to override (`HeroDetailComponent`) and an override metadata object.
|
||||||
|
The [overide metadata object](#metadata-override-object) is a generic defined as follows:
|
||||||
|
|
||||||
|
code-example(format="." language="javascript").
|
||||||
|
type MetadataOverride<T> = {
|
||||||
|
add?: T;
|
||||||
|
remove?: T;
|
||||||
|
set?: T;
|
||||||
|
};
|
||||||
|
:marked
|
||||||
|
A metadata override object can either add-and-remove elements in metadata properties or completely reset those properties.
|
||||||
|
This example resets the component's `providers` metadata.
|
||||||
|
|
||||||
|
The type parameter, `T`, is the kind of metadata you'd pass to the `@Component` decorator:
|
||||||
|
code-example(format="." language="javascript").
|
||||||
|
selector?: string;
|
||||||
|
template?: string;
|
||||||
|
templateUrl?: string;
|
||||||
|
providers?: any[];
|
||||||
|
...
|
||||||
|
|
||||||
|
#stub-hero-detail-service
|
||||||
|
:marked
|
||||||
|
### _StubHeroDetailService_
|
||||||
|
|
||||||
|
This example completely replaces the component's `providers` with an array containing the `StubHeroDetailService`.
|
||||||
|
The `StubHeroDetailService` is dead simple. It doesn't need a `HeroService` (fake or otherwise).
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'stub-hds', 'app/hero/hero-detail.component.spec.ts (StubHeroDetailService)')(format='.')
|
||||||
|
:marked
|
||||||
|
### The override tests
|
||||||
|
|
||||||
|
Now the tests can control the component's hero directly by manipulating the stub's `testHero`.
|
||||||
|
+makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-tests', 'app/hero/hero-detail.component.spec.ts (override tests)')(format='.')
|
||||||
|
:marked
|
||||||
|
### More overrides
|
||||||
|
The `TestBed.overrideComponent` method can be called multiple times for the same or different components.
|
||||||
|
The `TestBed` offers similar `overrideDirective`, `overrideModule`, and `overridePipe` methods
|
||||||
|
for digging into and replacing parts of these other classes.
|
||||||
|
|
||||||
|
Explore the options and combinations on your own.
|
||||||
|
|
||||||
a(href="#top").to-top Back to top
|
a(href="#top").to-top Back to top
|
||||||
.l-hr
|
.l-hr
|
||||||
|
|
||||||
|
@ -1299,8 +1448,7 @@ a(href="#top").to-top Back to top
|
||||||
### What good are these tests?
|
### What good are these tests?
|
||||||
|
|
||||||
Stubbed `RouterLink` tests can confirm that a component with links and an outlet is setup properly,
|
Stubbed `RouterLink` tests can confirm that a component with links and an outlet is setup properly,
|
||||||
that the component has the links it should have and
|
that the component has the links it should have, and that they are all pointing in the expected direction.
|
||||||
that they are all pointing in the expected direction.
|
|
||||||
These tests do not concern whether the app will succeed in navigating to the target component when the user clicks a link.
|
These tests do not concern whether the app will succeed in navigating to the target component when the user clicks a link.
|
||||||
|
|
||||||
Stubbing the RouterLink and RouterOutlet is the best option for such limited testing goals.
|
Stubbing the RouterLink and RouterOutlet is the best option for such limited testing goals.
|
||||||
|
@ -1664,6 +1812,7 @@ code-example(format="." language="javascript").
|
||||||
schemas?: Array<SchemaMetadata | any[]>;
|
schemas?: Array<SchemaMetadata | any[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#metadata-override-object
|
||||||
:marked
|
:marked
|
||||||
Each overide method takes a `MetadataOverride<T>` where `T` is the kind of metadata
|
Each overide method takes a `MetadataOverride<T>` where `T` is the kind of metadata
|
||||||
appropriate to the method, the parameter of an `@NgModule`, `@Component`, `@Directive`, or `@Pipe`.
|
appropriate to the method, the parameter of an `@NgModule`, `@Component`, `@Directive`, or `@Pipe`.
|
||||||
|
@ -1913,11 +2062,6 @@ table
|
||||||
From the test root component's `DebugElement`, returned by `fixture.debugElement`,
|
From the test root component's `DebugElement`, returned by `fixture.debugElement`,
|
||||||
you can walk (and query) the fixture's entire element and component sub-trees.
|
you can walk (and query) the fixture's entire element and component sub-trees.
|
||||||
|
|
||||||
.alert.is-important
|
|
||||||
:marked
|
|
||||||
The _DebugElement_ is officially _experimental_ and thus subject to change.
|
|
||||||
Consult the [API reference](../api/core/index/DebugElement-class.html) for the latest status.
|
|
||||||
:marked
|
|
||||||
Here are the most useful `DebugElement` members for testers in approximate order of utility.
|
Here are the most useful `DebugElement` members for testers in approximate order of utility.
|
||||||
|
|
||||||
table
|
table
|
||||||
|
|
Loading…
Reference in New Issue