- 6 minutes read

There are two ways to test an Angular service. Either you test it as a regular class. Or you use the entire machinery of Angular. If you do the latter, ng-mocks is your friend.

Mocking a service using Angular and ng-mocks

I've published the sourcecode on GitHub. Clone it to play with the mocking framework to get the knack of it!
The tradional approach uses the TestBed. The first example replaces the ContinentService by a mock implementation. The ContinentService, in turn, is a part of my current Angular course. Among other things, it fetches the list of countries from https://restcountries.com/ and groups them by continent. At the moment, we only want to mock the service fetching the countries and storing them in an Observable called countries$:

const countries = [{ name: "España", region: "Europa", population: 47000000, flag: "https://flags.com/spain"} ]; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [MockProvider(ContinentService, { countries$: of>(countries) }] ], }).compileComponents(); }); it('should be created', async () => { const result = await service.countries$.toPromise(); expect(httpClientMock.get).toHaveBeenCalled(); expect(result[0].name).toBe("España"); });

You'll notice this example is tautological. It only tests the mocking framework. In a real-life application, ContinentService is a transitive dependency.

Mocking a service using the standard Angular tools

Like most Angular services, our ContinentService uses the httpClient to access the backend. Instead of mocking the ContinentService, it's often a better idea to leave it as it is and to mock the HttpClient. This way, you also test the implementation of the service. Angular offers the HttpClientTestingModule to "mock away" the backend:

describe('ContinentService', () => { let service: ContinentService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule] }); service = TestBed.inject(ContinentService); }); it('should be created', async () => { expect(service).toBeTruthy(); const httpMock = TestBed.inject(HttpTestingController); const countries = [ { name: 'España', region: 'Europa', population: 47000000, flag: 'https://flags.com/spain', }, ]; // unfortunately, you can't use async/await! // const result = await service.countries$.toPromise(); service.countries$.subscribe((result) => { expect(result[0].name).toBe('España'); }); const mockRequest = httpMock.expectOne('https://restcountries.com/v2/all'); mockRequest.flush(countries); }); });

Why I prefer not to use the Angular engine

Looking at the code above, you'll notice it's sort of upside-down. First, you subscribe to the HTTP call under test. After that, you define what happens when the REST call is executed. The test is hidden in the subscription, but it doesn't run before the last line is completed despite being above. But that's probably just a matter of convention. After a while, it comes naturally to you.

More annoying is you can't use the async/await pattern. That's a direct consequence of the upside-down definition. That isn't very pleasant, but it's not a big deal.

A more pressing issue is speed. You want Jest to execute fast. That was the reason to pick Jest in the first place. I didn't run a benchmark, but it stands to reason that compiling a testbed takes more time than performing a simple new HttpClient().

But that's still not the issue I'm worried about. The idea of mocking is to simplify your application as much as possible. The Angular engine adds complexity to your test, so let's eliminate it—one layer of abstraction less.

On the other hand, your Angular application always uses the Angular machinery, so arguably, getting rid of it is precisely what you don't want to do.

It's up to you. Chose the option that's easier to implement and to maintain. If in doubt, use the option that runs faster.

Mocking Angular services without using Angular

However, using the TestBed adds a layer of complexity to your test. Generally speaking, testing simple classes is easier than testing a service that needs the entire infrastructure of Angular. If it's an option, write your tests using the good old new keyword.

Sometimes that's easy. A few methods of your service don't depend on other services. That makes testing easy.

When a service depends on other services, the general strategy is to mock them away. That, in turn, means you don't need the machinery of Angular. I can think of two strategies that don't require Angular's dependency injection.

The first option is to use jest.mock():

import { of } from 'rxjs'; import { ContinentService } from './continent.service'; const countries = [ { name: 'España', region: 'Europa', population: 47000000, flag: 'https://flags.com/spain', }, ]; jest.mock('./continent.service'); ContinentService.prototype.countries$ = of(countries); const service = new ContinentService(null); describe('ContinentService', () => { it('should be created', async () => { const result = await service.countries$.toPromise(); expect(result[0].name).toBe('España'); }); });

This approach modifies the JavaScript module. Putting it into simpler words, it modifies the class ContinentService. The beauty of this approach is it also works when the service is used indirectly. In particular, it plays well with Angular's dependency injection.

Mocking a service using ng-mocks (part 2)

A less intrusive approach uses ng-mock's MockService. The example below mocks Angular's httpClient.get. When I tried to implement the demo, I ran into trouble. The TypeScript implementation of httpClient is too clever for my taste. It uses optional generics. That allowed Angular to add a nice feature (http.get>, which unpacks the JSON object) while being compatible to the older API (http.get(), which returns the JSON object as a string). That's awesome, but it forced my to use any to convince the compiler my code is correct. If you know how to do it right, please leave a comment!

const countries = [{ name: "España", region: "Europa", population: 47000000, flag: "https://flags.com/spain"} ]; const httpClientMock = MockService(HttpClient); httpClientMock.get = (jest.fn(() => of(countries as Array))) as any; // sorry about using any! ^^^ const service = new ContinentService(httpClientMock); describe('ContinentService', () => { it('should be created', async () => { const result = await service.countries$.toPromise(); expect(httpClientMock.get).toHaveBeenCalled(); expect(result[0].name).toBe("España"); }); });

Strictly speaking, my example doesn't require you to use ng-mocks. Here's the plain vanilla version:

const httpClientMock = new HttpClient(null); httpClientMock.get = (jest.fn(() => of(countries as Array))) as any;

The advantage of using MockService is it mocks every method. So you're 100% sure it never attempts to access the backend. new HttpClient(null) can't access the backend, either, because of the null parameter - but if it tries for some reason, it'll raise an exception.


Comments