The ability to stub out services is useful, but it can be limiting in a number of ways. It can also be tedious, as the stubs you create must remain up to date with the public interface of the service. Another excellent tool at your disposal when writing unit tests is the spy.
A spy allows you to select a function or method. It also helps you collect information about if and how it was invoked as well as how it will behave once it is invoked. It is similar in concept to a stub but allows you to have a much more robust unit test.
The code, links, and a live example related to this recipe are available at .
Begin with the component tests you wrote in the last recipe:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallServiceStub = { reveal: () => magicEightBallResponse }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ { provide: MagicEightBallService, useValue: magicEightBallServiceStub } ] }); fixture = TestBed.createComponent(MagicEightBallComponent); })); afterEach(() => { fixture = undefined; }); it('should begin with no text', async(() => { fixture.detectChanges(); expect(getHeaderEl().textContent).toEqual(''); })); it('should show text after click', async(() => { fixture.debugElement.query(By.css('button')) .triggerEventHandler('click', null); fixture.detectChanges(); expect(getHeaderEl().textContent) .toEqual(magicEightBallResponse); })); });
Instead of using a stub, configure the test module to provide the actual service:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ MagicEightBallService ] }); fixture = TestBed.createComponent(MagicEightBallComponent); })); afterEach(() => { fixture = undefined; }); it('should begin with no text', async(() => { fixture.detectChanges(); expect(getHeaderEl().textContent).toEqual(''); })); it('should show text after click', async(() => { fixture.debugElement.query(By.css('button')) .triggerEventHandler('click', null); fixture.detectChanges(); expect(getHeaderEl().textContent) .toEqual(magicEightBallResponse); })); });
Your goal is to use a method spy to intercept calls to reveal()
on the service. The problem here, however, is that the service is being injected into the component; therefore, you don't have a direct ability to get a reference to the service instance and set a spy on it. Fortunately, the component fixture provides this for you:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallService; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ MagicEightBallService ] }); fixture = TestBed.createComponent(MagicEightBallComponent); magicEightBallService = fixture.debugElement.injector .get(MagicEightBallService); })); afterEach(() => { fixture = undefined; magicEightBallService = undefined; }); ... });
Next, set a spy on the service instance using spyOn()
. Configure the spy to intercept the method call and return the static value instead:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallService; let revealSpy; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ MagicEightBallService ] }); fixture = TestBed.createComponent(MagicEightBallComponent); magicEightBallService = fixture.debugElement.injector .get(MagicEightBallService); revealSpy = spyOn(magicEightBallService, 'reveal') .and.returnValue(magicEightBallResponse); })); afterEach(() => { fixture = undefined; magicEightBallService = undefined; revealSpy = undefined; }); ... });
With this spy, you are now capable of seeing how the rest of the application interacts with this captured method. Add a new test, and check that the method is called once and returns the proper value after a click (this also pulls the clicking action into its own test helper):
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallService; let revealSpy; let clickButton = () => { fixture.debugElement.query(By.css('button')) .triggerEventHandler('click', null); }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ MagicEightBallService ] }); fixture = TestBed.createComponent(MagicEightBallComponent); magicEightBallService = fixture.debugElement.injector .get(MagicEightBallService); revealSpy = spyOn(magicEightBallService, 'reveal') .and.returnValue(magicEightBallResponse); })); afterEach(() => { fixture = undefined; magicEightBallService = undefined; revealSpy = undefined; }); it('should begin with no text', async(() => { fixture.detectChanges(); expect(getHeaderEl().textContent).toEqual(''); })); it('should call reveal after a click', async(() => { clickButton(); expect(revealSpy.calls.count()).toBe(1); expect(revealSpy.calls.mostRecent().returnValue) .toBe(magicEightBallResponse); })); it('should show text after click', async(() => { clickButton(); fixture.detectChanges(); expect(getHeaderEl().textContent) .toEqual(magicEightBallResponse); })); });
Note that detectChanges()
is only required to resolve the data binding, not to execute event handlers.
Jasmine spies act as method interceptors and are capable of inspecting everything about the given method invocation. It can track if and when a method was called, what arguments it was called with, how many times it was called, how it should behave, and so on. This is extremely useful when trying to remove dependencies from component unit tests, as you can mock out the public interface of the service using spies.
Spies are not beholden to replace the method outright. Here, it is useful to be able to prevent the execution from reaching the internals of the service, but it is not difficult to imagine cases where you would only want to passively observe the invocation of a certain method and allow the execution to continue normally.
For such a purpose, instead of using .and.returnValue()
, Jasmine allows you to use .and.callThrough()
, which will allow the execution to proceed uninterrupted.