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 stub-hds
|
// #enddocregion hds-spy
|
||||||
|
|
||||||
// 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;
|
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
|
||||||
const hds = compInjector.get(HeroDetailService);
|
this.navSpy = spyOn(router, 'navigate');
|
||||||
const router = compInjector.get(Router);
|
|
||||||
|
|
||||||
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 */
|
/** 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…
Reference in New Issue