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.
- How to Set Up Angular Unit Testing with Jest
- Testing Angular applications with Jest and Spectator
- How I do configure Jest to test my Angular 8 Project
- https://github.com/thymikee/jest-preset-angular
- Unit Testing Angular with Jest
- Migrate your Angular library to Jest
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,
- https://github.com/ngneat/spectator
- Spectator V4: A Powerful Tool to Simplify Your Angular Tests!
- Spectator for Angular or: How I Learned to Stop Worrying and Love the Spec
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<HttpEvent<any>> { 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