DEV Community

Cover image for Testing Angular 10 Interceptors with Spectator and Jest
Ry
Ry

Posted on • Originally published at tenmilesquare.com

Testing Angular 10 Interceptors with Spectator and Jest

There are many reasons to switch from Karma and Jasmine to Jest when Testing Angular:

  • Jest runs faster than Karma and Jasmine
  • Jest supports snapshot testing
  • Jest runs tests in parallels
  • Jest does not require a browser for testing
  • many more...

However, what's missing are examples of how to write Angular unit tests in Jest, particularly testing Angular HTTP Interceptors.

Setting up Angular, Spectator, and Jest

For the purpose of this article, we will assume that you have an Angular project already set up with Spectator and Jest. If not, I will provide you with some links on how to setup Angular with these libraries.

Jest

While the focus of this post is NOT on how to convert Angular from Karma and Jasmine to Jest, below is a list of resources on how to do this conversion yourself. You can also use my Github project as a template. I should mention that Jest can be a bit quirky if you are used to using other testing frameworks, but these quirks are worth it.

Spectator

Spectator is an amazing library that reduces the wordy boilerplate code for setting up Angular Unit Tests to only a few lines. It has a few quirks that are absolutely worth it for the value it provides,

A Couple of things

The major thing to keep in mind when using Spectator and jest together is that Specator imports should come from the @ngneat/spectator/jest package.

import {createHttpFactory, HttpMethod, SpectatorHttp} from '@ngneat/spectator/jest';

Below is the final devDependencies section of package.json.

{
ā€¦
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.7",
"@angular/cli": "~9.1.7",
"@angular/compiler-cli": "~9.1.9",
"@ngneat/spectator": "^5.13.3",
"@types/jest": "^26.0.13",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"jest": "^26.4.2",
"jest-preset-angular": "^8.3.1",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~3.8.3"
}
}

Angular 10 Interceptor Unit Test

For this example, we will be testing an Http Interceptor that logs HttpErrorResponses to the console.

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor, HttpErrorResponse, HttpResponse
} from '@angular/common/http';
import { Observable, throwError} from 'rxjs';
import {catchError, tap} from 'rxjs/operators';

/**
 * Intercepts HttpRequests and logs any http responses of 3xx+
 * In the future we can make this a conditional retry based on the status code.
 *
 */
@Injectable({ providedIn: 'root' })
export class HttpErrorInterceptor implements HttpInterceptor {

  constructor() {}

  intercept(req: HttpRequest, next: HttpHandler): Observable> {
    return next.handle(req).pipe(tap(() => {}),
      catchError((error) => {
        if (error instanceof HttpErrorResponse) {
          if (error.error && error.error.message) {
            console.log('status: ' + error.status + '\nmessage: ' + error.error.message);
          } else {
            console.log(error);
          }
        }
        return throwError(error);
      })
    );
  }
}

What this code does is intercept an HttpRequest from the application and logs the response to the console when an HttpErrorResponse is returned. The HttpHandler is used to execute the request next.handle. Then we create a pipe in order to tap the response for processing. Note: tap is a rxjs pipe function that allows us to inspect the data without changing the actual data in the pipe.

In this case, we catch the HttpErrorResponse, allowing any non-error HttpResponse to pass through. Once the Response is caught we can inspect the error message and log it to console. Note in this case we are expecting a custom body in the HttpResponse.

The Unit Test

In this unit test, we will be checking that a response with a 2xx will pass through and that an Error Response will be thrown. For more advanced testing the console could be mocked and we can check that the console.log has been called. This is out of scope for this article.

import {HttpErrorInterceptor} from './http-error.interceptor';
import {createHttpFactory, HttpMethod, SpectatorHttp} from '@ngneat/spectator/jest';
import {async} from '@angular/core/testing';
import {of, throwError} from 'rxjs';
import {HttpErrorResponse, HttpRequest, HttpResponse} from '@angular/common/http';
describe('HttpErrorInterceptor', () => {
let spectator: SpectatorHttp<HttpErrorInterceptor>;
const createHttp = createHttpFactory({
service: HttpErrorInterceptor
});
beforeEach(() => {
spectator = createHttp();
});
test('Http error', async(() => {
const mockHandler = {
handle: jest.fn(() => throwError(
new HttpErrorResponse({status: 500, error: {message: 'This is an error'}})))
};
spectator.service.intercept(new HttpRequest<unknown>(HttpMethod.GET, '/thing'), mockHandler)
.subscribe((response) => {
fail('Expected error');
}, (error => {
expect(error).toBeTruthy();
}));
}));
test('Http success', async(() => {
const mockHandler = {
handle: jest.fn(() => of(new HttpResponse({status: 500})))
};
spectator.service.intercept(new HttpRequest<unknown>(HttpMethod.GET, '/thing'), mockHandler)
.subscribe((response) => {
expect(response).toBeTruthy();
}, (error => {
fail('Expected Successful');
}));
}));
});

The key here is 1) how the handler is mocked and 2) and how we test the interceptor response.

Mocking the HttpHandler

The first confusing thing when testing the interceptor is how to mock the HttpHandler. Since Jasmine is removed mock and SpyOn are off the table. You may notice that jest.mock exists, but it doesn't function as expected. This is one of those little Jest quirks I mentioned; jest.mock is used to mock a package and not an object. In this case, we will build an object that looks like HttpHandler interface and mock the methods expected. Below is the HttpHandler interface. As you can see it only has one method.

export declare abstract class HttpHandler {
abstract handle(req: HttpRequest): Observable<HttpEvent<any>>;
}

This is easily mocked with jest.fn()

const mockHandler = {
handle: jest.fn(() => throwError(
new HttpErrorResponse({status: 500, error: {message: 'This is an error'}})))
};

In the error case, we will instruct the method to throw an HttpErrorResponse and create a custom object for the response body/error.

In the happy path case the mock looks like the following:

const mockHandler = {
handle: jest.fn(() => of(new HttpResponse({status: 200})))
};

Testing the Interceptor's Response

Now that we have the HttpHandler mocked, how do we actually test that the interceptor does anything? The key here is to specify an input on the .subscribe lambda.

spectator.service.intercept(new HttpRequest<unknownn>(HttpMethod.GET, '/thing'), mockHandler)
.subscribe((response) => {
expect(response).toBeTruthy();
}, (error => {
fail('Expected Successful');
}));

In this case we are checking that that the interceptor passed the response through as normal, and did not throw an error.

Spectator and Unit Testing Fiddly Bits

Some might note that the code is using spectators createHttpFactory instead of createServiceFactory. In this scenario, both will work exactly the same. I'm using createHttpFactory in anticipation of adding an HTTP retry.

It is also important to note that this interceptor doesn't actually modify the Response and the tests are a bit weak. This is meant to be a basic framework to get you started with testing interceptors. If you have an interceptor that modifies the HttpRespond using map, you will be able to specify the input using the mocked HttpHandler and test the output in the subscribe portion of the interceptor call.

Summary

Using Spectator and Jest with Angular 10 is a very powerful combination. The trick is to either have a full understanding of Jest and Spectator, or have a ready source of examples to draw from. I hope this article can provide you a rough understanding of how to use Jest in concert with Spectator to test Angular HttpInterceptors. The keys here are

  • Using jest.fn() to mock the function of the HttpHandler
  • Adding the input variable to the subscribe lambda for testing

Github source: https://github.com/djchi82/angular-jest-spectator-interceptor-test

Top comments (1)

Collapse
 
ryboflavin42 profile image
Ry

BTW I'm aware, a bit late, that the package.json specifies Angular 9 and the article say Angular 10. The original project was in 10 and when I created the straw-man project for this article I accidentally create the project with Angular 9. The mechanics of this article still hold up for Angular 10. Nothing changes between the versions.