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() {
// #docregion stub-hds
class StubHeroDetailService {
// #docregion hds-spy
class HeroDetailServiceSpy {
testHero = new Hero(42, 'Test Hero');
getHero(id: number | string): Promise<Hero> {
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<Hero> {
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);
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
this.saveSpy = spyOn(hds, 'saveHero').and.callThrough();
}
/** Add page elements after hero arrives */

View File

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