diff --git a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts index 03eb393c33..297e5d7ef8 100644 --- a/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts +++ b/public/docs/_examples/testing/ts/app/hero/hero-detail.component.spec.ts @@ -34,19 +34,23 @@ describe('HeroDetailComponent', () => { //////////////////// function overrideSetup() { - // #docregion stub-hds - class StubHeroDetailService { + // #docregion hds-spy + class HeroDetailServiceSpy { testHero = new Hero(42, 'Test Hero'); - getHero(id: number | string): Promise { - return Promise.resolve(true).then(() => Object.assign({}, this.testHero) ); - } + getHero = jasmine.createSpy('getHero').and.callFake( + () => Promise + .resolve(true) + .then(() => Object.assign({}, this.testHero)) + ); - saveHero(hero: Hero): Promise { - return Promise.resolve(true).then(() => Object.assign(this.testHero, hero) ); - } + saveHero = jasmine.createSpy('saveHero').and.callFake( + (hero: Hero) => Promise + .resolve(true) + .then(() => Object.assign(this.testHero, hero)) + ); } - // #enddocregion stub-hds + // #enddocregion hds-spy // the `id` value is irrelevant because ignored by service stub beforeEach(() => activatedRoute.testParams = { id: 99999 } ); @@ -70,7 +74,7 @@ function overrideSetup() { .overrideComponent(HeroDetailComponent, { set: { providers: [ - { provide: HeroDetailService, useClass: StubHeroDetailService } + { provide: HeroDetailService, useClass: HeroDetailServiceSpy } ] } }) @@ -81,31 +85,37 @@ function overrideSetup() { // #enddocregion setup-override // #docregion override-tests - let hds: StubHeroDetailService; + let hdsSpy: HeroDetailServiceSpy; beforeEach( async(() => { createComponent(); - // get the component's injected StubHeroDetailService - hds = fixture.debugElement.injector.get(HeroDetailService); + // get the component's injected HeroDetailServiceSpy + 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', () => { - expect(page.nameDisplay.textContent).toBe(hds.testHero.name); + expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); }); it('should save stub hero change', fakeAsync(() => { - const origName = hds.testHero.name; + const origName = hdsSpy.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'); + expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save'); click(page.saveBtn); + expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once'); + 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'); })); // #enddocregion override-tests @@ -114,7 +124,7 @@ function overrideSetup() { inject([HeroDetailService], (service: HeroDetailService) => { 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', () => { + // 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); - 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'); }); @@ -322,7 +337,6 @@ function createComponent() { class Page { gotoSpy: jasmine.Spy; navSpy: jasmine.Spy; - saveSpy: jasmine.Spy; saveBtn: DebugElement; cancelBtn: DebugElement; @@ -330,14 +344,9 @@ class Page { nameInput: HTMLInputElement; constructor() { - // Use component's injector to see the services it injected. - const compInjector = fixture.debugElement.injector; - const hds = compInjector.get(HeroDetailService); - const router = compInjector.get(Router); - - this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); - this.navSpy = spyOn(router, 'navigate'); - this.saveSpy = spyOn(hds, 'saveHero').and.callThrough(); + const router = TestBed.get(Router); // get router from root injector + this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough(); + this.navSpy = spyOn(router, 'navigate'); } /** Add page elements after hero arrives */ diff --git a/public/docs/ts/latest/guide/testing.jade b/public/docs/ts/latest/guide/testing.jade index df7deb4f1b..d4999a9ad4 100644 --- a/public/docs/ts/latest/guide/testing.jade +++ b/public/docs/ts/latest/guide/testing.jade @@ -55,8 +55,10 @@ a#top - [Testing with the _Observable_ test double](#tests-w-observable-double) * [Use a _page_ object to simplify setup](#page-object) * [Setup with module imports](#import-module) - * [Override component providers](#component-override) -

+

+ * [Override a component's providers](#component-override) + - [_overrideComponent_](#override-component-method) + - [Provide a _spy-stub_](#spy-stub) * [Test a _RouterOutlet_ component](#router-outlet-component) - [stubbing unneeded components](#stub-component) - [Stubbing the _RouterLink_](#router-link-stub) @@ -664,7 +666,7 @@ a#testbed-get :marked 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. :marked ### Always get the service from an injector @@ -734,7 +736,12 @@ a#service-spy .l-sub-section :marked 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 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)') @@ -1196,7 +1203,7 @@ figure.image-display * to wait until a `hero` arrives before `*ngIf` allows any element in DOM * element references for the title name span and name input-box to inspect their values * two button references to click - * spies on services and component methods + * 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. @@ -1309,7 +1316,7 @@ a#component-override 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). + Notice that `TestBed.configureTestingModule` no longer provides a (fake) `HeroService` because it's [not needed](#spy-stub). a#override-component-method :marked @@ -1339,17 +1346,26 @@ code-example(format="." language="javascript"). providers?: any[]; ... -a#stub-hero-detail-service +a#spy-stub :marked - ### _StubHeroDetailService_ + ### Provide a _spy stub_ (_HeroDetailServiceSpy_) - 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='.') + This example completely replaces the component's `providers` array with a new array containing a `HeroDetailServiceSpy`. + + 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 ### 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='.') :marked ### More overrides