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.
The code, links, and a live example related to this recipe are available at .
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:
The way your unit tests are written should account for these as well as completely test the behavior of this service.
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.
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.
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
.
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.
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.
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.