Книга: Angular 2 Cookbook
Назад: Unit testing a component with a service dependency using stubs
Дальше: 10. Performance and Advanced Concepts

Unit testing a component with a service dependency using spies

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.

Note

The code, links, and a live example related to this recipe are available at .

Getting ready

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);     }));   });   

How to do it...

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);     }));   });   

Setting a spy on the injected service

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

Note that detectChanges() is only required to resolve the data binding, not to execute event handlers.

How it works...

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.

There's more...

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.

See also

  • Writing a minimum viable unit test suite for a simple component shows you a basic example of unit testing Angular 2 components
  • Unit testing a synchronous service demonstrates how injection is mocked in unit tests
  • Unit testing a component with a service dependency using stubs shows how you can create a service mock to write unit tests and avoid direct dependencies
Назад: Unit testing a component with a service dependency using stubs
Дальше: 10. Performance and Advanced Concepts

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already