docs(testing): make the StubHeroDetailService a spy-stub (#2935)
This commit is contained in:
		
							parent
							
								
									499d4b3c88
								
							
						
					
					
						commit
						cd80df8dc7
					
				| @ -34,19 +34,23 @@ describe('HeroDetailComponent', () => { | |||||||
| 
 | 
 | ||||||
| ////////////////////
 | ////////////////////
 | ||||||
| function overrideSetup() { | function overrideSetup() { | ||||||
|   // #docregion stub-hds
 |   // #docregion hds-spy
 | ||||||
|   class StubHeroDetailService { |   class HeroDetailServiceSpy { | ||||||
|     testHero = new Hero(42, 'Test Hero'); |     testHero = new Hero(42, 'Test Hero'); | ||||||
| 
 | 
 | ||||||
|     getHero(id: number | string): Promise<Hero>  { |     getHero = jasmine.createSpy('getHero').and.callFake( | ||||||
|       return Promise.resolve(true).then(() => Object.assign({}, this.testHero) ); |       () => Promise | ||||||
|     } |         .resolve(true) | ||||||
|  |         .then(() => Object.assign({}, this.testHero)) | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     saveHero(hero: Hero): Promise<Hero> { |     saveHero = jasmine.createSpy('saveHero').and.callFake( | ||||||
|       return Promise.resolve(true).then(() => Object.assign(this.testHero, hero) ); |       (hero: Hero) => Promise | ||||||
|  |         .resolve(true) | ||||||
|  |         .then(() => Object.assign(this.testHero, hero)) | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|   } |   // #enddocregion hds-spy
 | ||||||
|   // #enddocregion stub-hds
 |  | ||||||
| 
 | 
 | ||||||
|   // the `id` value is irrelevant because ignored by service stub
 |   // the `id` value is irrelevant because ignored by service stub
 | ||||||
|   beforeEach(() => activatedRoute.testParams = { id: 99999 } ); |   beforeEach(() => activatedRoute.testParams = { id: 99999 } ); | ||||||
| @ -70,7 +74,7 @@ function overrideSetup() { | |||||||
|     .overrideComponent(HeroDetailComponent, { |     .overrideComponent(HeroDetailComponent, { | ||||||
|       set: { |       set: { | ||||||
|         providers: [ |         providers: [ | ||||||
|           { provide: HeroDetailService, useClass: StubHeroDetailService } |           { provide: HeroDetailService, useClass: HeroDetailServiceSpy } | ||||||
|         ] |         ] | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
| @ -81,31 +85,37 @@ function overrideSetup() { | |||||||
|   // #enddocregion setup-override
 |   // #enddocregion setup-override
 | ||||||
| 
 | 
 | ||||||
|   // #docregion override-tests
 |   // #docregion override-tests
 | ||||||
|   let hds: StubHeroDetailService; |   let hdsSpy: HeroDetailServiceSpy; | ||||||
| 
 | 
 | ||||||
|   beforeEach( async(() => { |   beforeEach( async(() => { | ||||||
|     createComponent(); |     createComponent(); | ||||||
|     // get the component's injected StubHeroDetailService
 |     // get the component's injected HeroDetailServiceSpy
 | ||||||
|     hds = fixture.debugElement.injector.get(HeroDetailService); |     hdsSpy = fixture.debugElement.injector.get(HeroDetailService); | ||||||
|   })); |   })); | ||||||
| 
 | 
 | ||||||
|  |   it('should have called `getHero`', () => { | ||||||
|  |     expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   it('should display stub hero\'s name', () => { |   it('should display stub hero\'s name', () => { | ||||||
|     expect(page.nameDisplay.textContent).toBe(hds.testHero.name); |     expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('should save stub hero change', fakeAsync(() => { |   it('should save stub hero change', fakeAsync(() => { | ||||||
|     const origName = hds.testHero.name; |     const origName = hdsSpy.testHero.name; | ||||||
|     const newName = 'New Name'; |     const newName = 'New Name'; | ||||||
| 
 | 
 | ||||||
|     page.nameInput.value = newName; |     page.nameInput.value = newName; | ||||||
|     page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
 |     page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
 | ||||||
| 
 | 
 | ||||||
|     expect(comp.hero.name).toBe(newName, 'component hero has new name'); |     expect(comp.hero.name).toBe(newName, 'component hero has new name'); | ||||||
|     expect(hds.testHero.name).toBe(origName, 'service hero unchanged before save'); |     expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save'); | ||||||
| 
 | 
 | ||||||
|     click(page.saveBtn); |     click(page.saveBtn); | ||||||
|  |     expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once'); | ||||||
|  | 
 | ||||||
|     tick(); // wait for async save to complete
 |     tick(); // wait for async save to complete
 | ||||||
|     expect(hds.testHero.name).toBe(newName, 'service hero has new name after save'); |     expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save'); | ||||||
|     expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); |     expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called'); | ||||||
|   })); |   })); | ||||||
|   // #enddocregion override-tests
 |   // #enddocregion override-tests
 | ||||||
| @ -114,7 +124,7 @@ function overrideSetup() { | |||||||
|     inject([HeroDetailService], (service: HeroDetailService) => { |     inject([HeroDetailService], (service: HeroDetailService) => { | ||||||
| 
 | 
 | ||||||
|     expect(service).toEqual({}, 'service injected from fixture'); |     expect(service).toEqual({}, 'service injected from fixture'); | ||||||
|     expect(hds).toBeTruthy('service injected into component'); |     expect(hdsSpy).toBeTruthy('service injected into component'); | ||||||
|   })); |   })); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -164,8 +174,13 @@ function heroModuleSetup() { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should save when click save but not navigate immediately', () => { |     it('should save when click save but not navigate immediately', () => { | ||||||
|  |       // Get service injected into component and spy on its`saveHero` method.
 | ||||||
|  |       // It delegates to fake `HeroService.updateHero` which delivers a safe test result.
 | ||||||
|  |       const hds = fixture.debugElement.injector.get(HeroDetailService); | ||||||
|  |       const saveSpy = spyOn(hds, 'saveHero').and.callThrough(); | ||||||
|  | 
 | ||||||
|       click(page.saveBtn); |       click(page.saveBtn); | ||||||
|       expect(page.saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); |       expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called'); | ||||||
|       expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called'); |       expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -322,7 +337,6 @@ function createComponent() { | |||||||
| class Page { | class Page { | ||||||
|   gotoSpy:      jasmine.Spy; |   gotoSpy:      jasmine.Spy; | ||||||
|   navSpy:       jasmine.Spy; |   navSpy:       jasmine.Spy; | ||||||
|   saveSpy:      jasmine.Spy; |  | ||||||
| 
 | 
 | ||||||
|   saveBtn:      DebugElement; |   saveBtn:      DebugElement; | ||||||
|   cancelBtn:    DebugElement; |   cancelBtn:    DebugElement; | ||||||
| @ -330,14 +344,9 @@ class Page { | |||||||
|   nameInput:    HTMLInputElement; |   nameInput:    HTMLInputElement; | ||||||
| 
 | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     // Use component's injector to see the services it injected.
 |     const router = TestBed.get(Router); // get router from root injector
 | ||||||
|     const compInjector = fixture.debugElement.injector; |  | ||||||
|     const hds          = compInjector.get(HeroDetailService); |  | ||||||
|     const router       = compInjector.get(Router); |  | ||||||
| 
 |  | ||||||
|     this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); |     this.gotoSpy = spyOn(comp, 'gotoList').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 */ | ||||||
|  | |||||||
| @ -55,8 +55,10 @@ a#top | |||||||
|     - [Testing with the _Observable_ test double](#tests-w-observable-double) |     - [Testing with the _Observable_ test double](#tests-w-observable-double) | ||||||
|   * [Use a _page_ object to simplify setup](#page-object) |   * [Use a _page_ object to simplify setup](#page-object) | ||||||
|   * [Setup with module imports](#import-module) |   * [Setup with module imports](#import-module) | ||||||
|   * [Override component providers](#component-override) |  | ||||||
|   <br><br> |   <br><br> | ||||||
|  |   * [Override a component's providers](#component-override) | ||||||
|  |     - [_overrideComponent_](#override-component-method) | ||||||
|  |     - [Provide a _spy-stub_](#spy-stub) | ||||||
|   * [Test a _RouterOutlet_ component](#router-outlet-component) |   * [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) | ||||||
| @ -664,7 +666,7 @@ a#testbed-get | |||||||
|   :marked |   :marked | ||||||
|     The [`inject`](#inject)  utility function is another way to get one or more services from the test root injector. |     The [`inject`](#inject)  utility function is another way to get one or more services from the test root injector. | ||||||
|      |      | ||||||
|     See the section "[_Override Component Providers_](#component-override)" for a use case |     See the section "[_Override a component's providers_](#component-override)" for a use case | ||||||
|     in which `inject` and `TestBed.get` do not work and you must get the service from the component's injector. |     in which `inject` and `TestBed.get` do not work and you must get the service from the component's injector. | ||||||
| :marked | :marked | ||||||
|   ### Always get the service from an injector |   ### Always get the service from an injector | ||||||
| @ -734,7 +736,12 @@ a#service-spy | |||||||
| .l-sub-section | .l-sub-section | ||||||
|   :marked |   :marked | ||||||
|     Faking a service instance and spying on the real service are _both_ great options.  |     Faking a service instance and spying on the real service are _both_ great options.  | ||||||
|     Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind. |     Pick the one that seems easiest for the current test suite.  | ||||||
|  |     Don't be afraid to change your mind. | ||||||
|  | 
 | ||||||
|  |     Spying on the real service isn't always easy, especially when the real service has injected dependencies. | ||||||
|  |     You can _stub and spy_ at the same time, as shown in [an example below](#spy-stub). | ||||||
|  | 
 | ||||||
| :marked | :marked | ||||||
|   Here are the tests with commentary to follow: |   Here are the tests with commentary to follow: | ||||||
| +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)') | +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)') | ||||||
| @ -1196,7 +1203,7 @@ figure.image-display | |||||||
|   * to wait until a `hero` arrives before `*ngIf` allows any element in DOM |   * to wait until a `hero` arrives before `*ngIf` allows any element in DOM | ||||||
|   * element references for the title name span and name input-box to inspect their values |   * element references for the title name span and name input-box to inspect their values | ||||||
|   * two button references to click |   * two button references to click | ||||||
|   * spies on services and component methods |   * spies on component and router methods | ||||||
| 
 | 
 | ||||||
|   Even a small form such as this one can produce a mess of tortured conditional setup and CSS element selection. |   Even a small form such as this one can produce a mess of tortured conditional setup and CSS element selection. | ||||||
| 
 | 
 | ||||||
| @ -1309,7 +1316,7 @@ a#component-override | |||||||
|   as seen in the following setup variation: |   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='.') | +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-override', 'app/hero/hero-detail.component.spec.ts (Override setup)')(format='.') | ||||||
| :marked | :marked | ||||||
|   Notice that `TestBed.configureTestingModule` no longer provides a (fake) `HeroService` because it's [not needed](#stub-hero-detail-service). |   Notice that `TestBed.configureTestingModule` no longer provides a (fake) `HeroService` because it's [not needed](#spy-stub). | ||||||
| 
 | 
 | ||||||
| a#override-component-method | a#override-component-method | ||||||
| :marked | :marked | ||||||
| @ -1339,17 +1346,26 @@ code-example(format="." language="javascript"). | |||||||
|     providers?: any[]; |     providers?: any[]; | ||||||
|     ... |     ... | ||||||
| 
 | 
 | ||||||
| a#stub-hero-detail-service | a#spy-stub | ||||||
| :marked | :marked | ||||||
|   ### _StubHeroDetailService_ |   ### Provide a _spy stub_ (_HeroDetailServiceSpy_) | ||||||
| 
 | 
 | ||||||
|   This example completely replaces the component's `providers` with an array containing the `StubHeroDetailService`. |   This example completely replaces the component's `providers` array with a new array containing a `HeroDetailServiceSpy`. | ||||||
|   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='.') |   The `HeroDetailServiceSpy` is a stubbed version of the real `HeroDetailService`  | ||||||
|  |   that fakes all necessary features of that service.  | ||||||
|  |   It neither injects nor delegates to the lower level `HeroService`  | ||||||
|  |   so there's no need to provide a test double for that. | ||||||
|  | 
 | ||||||
|  |   The related `HeroDetailComponent` tests will assert that methods of the `HeroDetailService`  | ||||||
|  |   were called by spying on the service methods. | ||||||
|  |   Accordingly, the stub implements its methods as spies: | ||||||
|  | +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'hds-spy', 'app/hero/hero-detail.component.spec.ts (HeroDetailServiceSpy)')(format='.') | ||||||
| :marked | :marked | ||||||
|   ### The override tests |   ### The override tests | ||||||
| 
 | 
 | ||||||
|   Now the tests can control the component's hero directly by manipulating the stub's `testHero`. |   Now the tests can control the component's hero directly by manipulating the spy-stub's `testHero` | ||||||
|  |   and confirm that service methods were called. | ||||||
| +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-tests', 'app/hero/hero-detail.component.spec.ts (override tests)')(format='.') | +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-tests', 'app/hero/hero-detail.component.spec.ts (override tests)')(format='.') | ||||||
| :marked | :marked | ||||||
|   ### More overrides |   ### More overrides | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user