docs(testing): make the StubHeroDetailService a spy-stub (#2935)

This commit is contained in:
Ward Bell 2016-12-06 18:16:32 -08:00 committed by GitHub
parent 499d4b3c88
commit cd80df8dc7
2 changed files with 65 additions and 40 deletions

View File

@ -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 */

View File

@ -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