Книга: Angular 2 Cookbook
Назад: Writing a minimum viable end-to-end test suite for a simple application
Дальше: Unit testing a component with a service dependency using stubs

Unit testing a synchronous service

Angular 2 service types are essentially classes designated for injectability. They are easy to test since you have a great deal of control over how and where they are provided, and consequently, how many instances you'll be able to create. Therefore, tests for services will exist largely as they would for any normal TypeScript class.

It'll be better if you are familiar with the content of the first few recipes of this chapter before you proceed further.

Note

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

Getting ready

Suppose you want to build a "magic eight ball" service. Begin with the following code, with added comments for clarity:

[src/app/magic-eight-ball.service.ts]      import {Injectable} from '@angular/core';      @Injectable()   export class MagicEightBallService {     private values:Array<string>;     private lastIndex:number;        constructor() {       // Initialize the values array       // Must have at least two entries       this.values = [         'Ask again later',         'Outlook good',         'Most likely',         'Don't count on it'       ];          // Initialize with any valid index       this.lastIndex = this.getIndex();     }        private getIndex():number {       // Return a random index for this.values       return Math.floor(Math.random() * this.values.length);     }        reveal():string {       // Generate a new index       let newIdx = this.getIndex();   .       // Check if the index was the same one used last time       if (newIdx === this.lastIndex) {         // If so, shift up one (wrapping around) in the array         // This is still random behavior         newIdx = (++newIdx) % this.values.length;       }              // Save the index that you are now using       this.lastIndex = newIdx;          // Access the string and return it       return this.values[newIdx];     }   }   

There are several things to note about how this service behaves:

  • This service has several private members but only one public member method
  • The service is randomly selected from an array
  • The service shouldn't return the same value twice in a row

The way your unit tests are written should account for these as well as completely test the behavior of this service.

How to do it...

Begin by creating the framework of your test file:

[src/app/magic-eight-ball.service.spec.ts]      import {TestBed} from '@angular/core/testing';   import {MagicEightBallService} from      './magic-eight-ball.service';      describe('Service: MagicEightBall', () => {     beforeEach(() => {       TestBed.configureTestingModule({         providers: [           MagicEightBallService         ]       });     });   });   

So far, none of this should surprise you. MagicEightBallService is an injectable; it needs to be provided inside a module declaration, which is done here. However, to actually use it inside a unit test, you need to perform a formal injection since this is what would be required to access it from inside a component. This can be accomplished with inject:

[src/app/magic-eight-ball.service.spec.ts]      import {TestBed, inject} from '@angular/core/testing';   import {MagicEightBallService} from      './magic-eight-ball.service';      describe('Service: MagicEightBall', () => {     beforeEach(() => {       TestBed.configureTestingModule({         providers: [           MagicEightBallService         ]       });     });        it('should be able to be injected', inject([MagicEightBallService],        (magicEightBallService: MagicEightBallService) => {         expect(magicEightBallService).toBeTruthy();       })     );   });   

Off to a good start, but this doesn't actually test anything about what the service is doing. Next, write a test that ensures that a string of non-zero length is being returned:

[src/app/magic-eight-ball.service.spec.ts]      import {TestBed, inject} from '@angular/core/testing';   import {MagicEightBallService} from      './magic-eight-ball.service';      describe('Service: MagicEightBall', () => {     beforeEach(() => {       TestBed.configureTestingModule({         providers: [           MagicEightBallService         ]       });     });        it('should be able to be injected', inject([MagicEightBallService],        (magicEightBallService: MagicEightBallService) => {         expect(magicEightBallService).toBeTruthy();       })     );        it('should return a string with nonzero length',   inject([MagicEightBallService],       (magicEightBallService: MagicEightBallService) => {         let result = magicEightBallService.reveal();            expect(result).toEqual(jasmine.any(String));         expect(result.length).toBeGreaterThan(0);       })     );    });   

Finally, you should write a test to ensure that the two values returned are not the same. Since this method is random, you can run it until you are blue in the face and still not be totally sure. However, checking this 50 times in a row is a fine way to be fairly certain:

[src/app/magic-eight-ball.service.spec.ts]      import {TestBed, inject} from '@angular/core/testing';   import {MagicEightBallService} from      './magic-eight-ball.service';      describe('Service: MagicEightBall', () => {     beforeEach(() => {       TestBed.configureTestingModule({         providers: [           MagicEightBallService         ]       });     });        it('should be able to be injected', inject([MagicEightBallService],        (magicEightBallService: MagicEightBallService) => {         expect(magicEightBallService).toBeTruthy();       })     );        it('should return a string with nonzero length',    inject([MagicEightBallService],       (magicEightBallService: MagicEightBallService) => {         let result = magicEightBallService.reveal();            expect(result).toEqual(jasmine.any(String));         expect(result.length).toBeGreaterThan(0);       })     );        it('should not return the same value twice in a row',   inject([MagicEightBallService],       (magicEightBallService: MagicEightBallService) => {         let last;         for(let i = 0; i < 50; ++i) {           let next = magicEightBallService.reveal();           expect(next).not.toEqual(last);           last = next;         }       })     );   });   

Terrific! All these tests have passed; you've done a good job building some incremental and descriptive code coverage for your service.

How it works...

The inject test function performs dependency injection for you each time it is invoked, using the array of injectable classes passed as the first argument. The arrow function that is passed as its second argument will behave in essentially the same way as a component constructor, where you are able to use the magicEightBallService parameter as an instance of the service.

Tip

One important difference from how it is injected compared to a component constructor is that inside a component constructor, you would be able to use this.magicEightBallService right away. With respect to injection into unit tests, it does not automatically attach to this.

There's more...

Important considerations for unit testing are what tests should be written and how they should proceed. Respecting the boundaries of public and private members is essential. Since these tests are written in a way that only utilizes the public members of the service, the author is free to go about changing, extending, or refactoring the internals of the service without worrying about breaking or needing to update the tests. A well-designed class will be fully testable from its public interface.

Tip

This notion brings up an interesting philosophical point regarding unit testing. You should be able to describe the behavior of a well-formed service as a function of its public members. Similarly, a well-formed service should then be relatively easy to write unit tests, given that the former statement is true.

If it is then the case that you find your unit tests are difficult to write—for example, you are needing to reach into a private member of the service to test it properly—then consider the notion that your service might not be as well designed as it could be.

In short, if it's hard to test, then you might have written a class in a weird way.

Testing without injection

An observant developer will note here that the service you are testing doesn't have any meaningful dependence on injection. Injecting it into various places in the application surely provides it with a consistent way, but the service definition is wholly unaware of this fact. After all, instantiation is instantiation, and this service doesn't appear to be more than an injectable class. Therefore, it is certainly possible to not bother injecting the service at all and merely instantiating it using the new keyword:

[src/app/magic-eight-ball.service.spec.ts]      import {MagicEightBallService} from      './magic-eight-ball.service';      describe('Service: MagicEightBall', () => {     let magicEightBallService;        beforeEach(() => {       magicEightBallService = new MagicEightBallService();     });        it('should be able to be injected', () => {       expect(magicEightBallService).toBeTruthy();     });        it('should return a string with nonzero length', () => {       let result = magicEightBallService.reveal();          expect(result).toEqual(jasmine.any(String));       expect(result.length).toBeGreaterThan(0);     });        it('should not return the same value twice in a row', () => {       let last;       for(let i = 0; i < 50; ++i) {         let next = magicEightBallService.reveal();         expect(next).not.toEqual(last);         last = next;       }     });   });   

Of course, this requires that you keep track of whether the service cares about whether or not it has been injected anywhere.

See also

  • 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 spies shows how you can keep track of service method invocations inside a unit test
Назад: Writing a minimum viable end-to-end test suite for a simple application
Дальше: Unit testing a component with a service dependency using stubs

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