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 { | ||||
|   addMatchers, newEvent, | ||||
|   ActivatedRoute, ActivatedRouteStub, Router, RouterStub | ||||
|   ActivatedRoute, ActivatedRouteStub, newEvent, Router, RouterStub | ||||
| } from '../../testing'; | ||||
| 
 | ||||
| import { HEROES, FakeHeroService } from '../model/testing'; | ||||
| 
 | ||||
| import { HeroModule }          from './hero.module'; | ||||
| import { Hero }                from '../model'; | ||||
| import { HeroDetailComponent } from './hero-detail.component'; | ||||
| import { HeroDetailService }   from './hero-detail.service'; | ||||
| import { Hero, HeroService }   from '../model'; | ||||
| import { HeroModule }          from './hero.module'; | ||||
| 
 | ||||
| ////// Testing Vars //////
 | ||||
| let activatedRoute: ActivatedRouteStub; | ||||
| @ -24,20 +21,117 @@ let comp: HeroDetailComponent; | ||||
| let fixture: ComponentFixture<HeroDetailComponent>; | ||||
| let page: Page; | ||||
| 
 | ||||
| //////////  Tests  ////////////////////
 | ||||
| 
 | ||||
| ////// Tests //////
 | ||||
| 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(() => { | ||||
|     addMatchers(); | ||||
|     activatedRoute = new ActivatedRouteStub(); | ||||
|     createComponent(); | ||||
|     // get the component's injected StubHeroDetailService
 | ||||
|     hds = fixture.debugElement.injector.get(HeroDetailService); | ||||
|   })); | ||||
| 
 | ||||
|     TestBed.configureTestingModule({ | ||||
|       imports: [ HeroModule ], | ||||
|   it('should display stub hero\'s name', () => { | ||||
|     expect(page.nameDisplay.textContent).toBe(hds.testHero.name); | ||||
|   }); | ||||
| 
 | ||||
|       // DON'T RE-DECLARE because already declared in HeroModule
 | ||||
|       // declarations: [HeroDetailComponent, TitleCasePipe], // No!
 | ||||
|   it('should save stub hero change', fakeAsync(() => { | ||||
|     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: [ | ||||
|         { provide: ActivatedRoute, useValue: activatedRoute }, | ||||
|         { provide: HeroService,    useClass: FakeHeroService }, | ||||
| @ -46,13 +140,14 @@ describe('HeroDetailComponent', () => { | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   })); | ||||
|   // #enddocregion setup-hero-module
 | ||||
| 
 | ||||
|   // #docregion route-good-id
 | ||||
|   describe('when navigate to hero id=' + HEROES[0].id, () => { | ||||
|   describe('when navigate to existing hero', () => { | ||||
|     let expectedHero: Hero; | ||||
| 
 | ||||
|     beforeEach( async(() => { | ||||
|       expectedHero = HEROES[0]; | ||||
|       expectedHero = firstHero; | ||||
|       activatedRoute.testParams = { id: expectedHero.id }; | ||||
|       createComponent(); | ||||
|     })); | ||||
| @ -76,7 +171,7 @@ describe('HeroDetailComponent', () => { | ||||
| 
 | ||||
|     it('should navigate when click save and save resolves', fakeAsync(() => { | ||||
|       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'); | ||||
|     })); | ||||
| 
 | ||||
| @ -91,8 +186,7 @@ describe('HeroDetailComponent', () => { | ||||
|       // dispatch a DOM event so that Angular learns of input value change.
 | ||||
|       page.nameInput.dispatchEvent(newEvent('input')); | ||||
| 
 | ||||
|       // detectChanges() makes [(ngModel)] push input value to component property
 | ||||
|       // and Angular updates the output span through the title pipe
 | ||||
|       // Tell Angular to update the output span through the title pipe
 | ||||
|       fixture.detectChanges(); | ||||
| 
 | ||||
|       expect(page.nameDisplay.textContent).toBe(titleCaseName); | ||||
| @ -131,10 +225,8 @@ describe('HeroDetailComponent', () => { | ||||
|   }); | ||||
|   // #enddocregion route-bad-id
 | ||||
| 
 | ||||
|   ///////////////////////////
 | ||||
| 
 | ||||
|   // 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; | ||||
|     fixture = TestBed.createComponent(HeroDetailComponent); | ||||
|     expect( | ||||
| @ -148,7 +240,64 @@ describe('HeroDetailComponent', () => { | ||||
|     service = fixture.debugElement.injector.get(HeroDetailService); | ||||
|     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 /////
 | ||||
| 
 | ||||
| @ -185,9 +334,10 @@ class Page { | ||||
|     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'); | ||||
|     this.saveSpy       = spyOn(hds, 'saveHero').and.callThrough(); | ||||
|   } | ||||
| 
 | ||||
|   /** Add page elements after hero arrives */ | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| /* tslint:disable:member-ordering */ | ||||
| // #docplaster
 | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, Router }   from '@angular/router'; | ||||
| import 'rxjs/add/operator/pluck'; | ||||
| @ -5,22 +7,24 @@ import 'rxjs/add/operator/pluck'; | ||||
| import { Hero }              from '../model'; | ||||
| import { HeroDetailService } from './hero-detail.service'; | ||||
| 
 | ||||
| // #docregion prototype
 | ||||
| @Component({ | ||||
|   selector: 'app-hero-detail', | ||||
|   selector:    'app-hero-detail', | ||||
|   templateUrl: 'app/hero/hero-detail.component.html', | ||||
|   styleUrls:  ['app/hero/hero-detail.component.css'], | ||||
|   providers:  [ HeroDetailService ] | ||||
| }) | ||||
| export class HeroDetailComponent implements OnInit { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   // #docregion ctor
 | ||||
|   constructor( | ||||
|     private heroDetailService: HeroDetailService, | ||||
|     private route: ActivatedRoute, | ||||
|     private route:  ActivatedRoute, | ||||
|     private router: Router) { | ||||
|   } | ||||
|   // #enddocregion ctor
 | ||||
| // #enddocregion prototype
 | ||||
| 
 | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   // #docregion ng-on-init
 | ||||
|   ngOnInit(): void { | ||||
| @ -50,4 +54,6 @@ export class HeroDetailComponent implements OnInit { | ||||
|   gotoList() { | ||||
|     this.router.navigate(['../'], {relativeTo: this.route}); | ||||
|   } | ||||
| // #docregion prototype
 | ||||
| } | ||||
| // #enddocregion prototype
 | ||||
|  | ||||
| @ -2,10 +2,13 @@ import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero, HeroService } from '../model'; | ||||
| 
 | ||||
| // #docregion prototype
 | ||||
| @Injectable() | ||||
| export class HeroDetailService { | ||||
|   constructor(private heroService: HeroService) {  } | ||||
| // #enddocregion prototype
 | ||||
| 
 | ||||
|   // Returns a clone which caller may modify safely
 | ||||
|   getHero(id: number | string): Promise<Hero> { | ||||
|     if (typeof id === 'string') { | ||||
|       id = parseInt(id as string, 10); | ||||
| @ -18,4 +21,6 @@ export class HeroDetailService { | ||||
|   saveHero(hero: Hero) { | ||||
|     return this.heroService.updateHero(hero); | ||||
|   } | ||||
| // #docregion prototype
 | ||||
| } | ||||
| // #enddocregion prototype
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { Hero }       from './hero'; | ||||
| import { HEROES }     from './test-heroes'; | ||||
| 
 | ||||
| @Injectable() | ||||
| /** Dummy HeroService that pretends to be real */ | ||||
| /** Dummy HeroService. Pretend it makes real http requests */ | ||||
| export class HeroService { | ||||
|   getHeroes() { | ||||
|     return Promise.resolve(HEROES); | ||||
| @ -21,9 +21,10 @@ export class HeroService { | ||||
| 
 | ||||
|   updateHero(hero: Hero): Promise<Hero> { | ||||
|     return this.getHero(hero.id).then(h => { | ||||
|       return h ? | ||||
|         Object.assign(h, hero) : | ||||
|         Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>; | ||||
|       if (!h) { | ||||
|         throw new Error(`Hero ${hero.id} not found`); | ||||
|       } | ||||
|       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.
 | ||||
| // Error.stackTraceLimit = Infinity; //
 | ||||
| 
 | ||||
| jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; | ||||
| jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; | ||||
| 
 | ||||
| var baseURL = document.baseURI; | ||||
| baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/'; | ||||
|  | ||||
| @ -52,6 +52,10 @@ block includes | ||||
|     - [_Observable_ test double](#stub-observable) | ||||
|   1. [Use a _page_ object to simplify setup](#page-object) | ||||
|   <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) | ||||
|     - [stubbing unneeded components](#stub-component) | ||||
|     - [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). | ||||
|    | ||||
|   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> | ||||
| <br><br> | ||||
| :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. | ||||
|   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='.') | ||||
| 
 | ||||
| a(href="#top").to-top Back to top | ||||
| .l-hr | ||||
| 
 | ||||
| #import-module | ||||
| :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 | ||||
| .l-hr | ||||
| 
 | ||||
| @ -1299,8 +1448,7 @@ a(href="#top").to-top Back to top | ||||
|   ### What good are these tests? | ||||
| 
 | ||||
|   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 they are all pointing in the expected direction. | ||||
|   that the component has the links it should have, and 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. | ||||
| 
 | ||||
|   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[]>; | ||||
|   }; | ||||
| 
 | ||||
| #metadata-override-object | ||||
| :marked | ||||
|   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`. | ||||
| @ -1913,11 +2062,6 @@ table | ||||
|   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. | ||||
| 
 | ||||
| .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. | ||||
| 
 | ||||
| table | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user