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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user