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
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$: ofYou'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:
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()
:
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!
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 ArrayThe 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.