DEV Community

seanbh
seanbh

Posted on • Originally published at Medium on

How to Unit Test an HttpInterceptor that Relies on NgRx


Photo by Nguyen Dang Hoang Nhu on Unsplash

In a previous post, we looked at how to setup an HttpInterceptor to add the Bearer token to the HTTP Authorization Header, when that token is stored in state with NgRx. In this post, we’ll look at how to unit test that interceptor with Jasmine/Karma.

Setting up beforeEach

Modify the beforeEach method of the test file as follows:

let httpTestingController: HttpTestingController;
let httpClient: HttpClient;
let appConfig: AppConfig;

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [
      AuthInterceptor,
      { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },      
      { provide: APP_CONFIG, useValue: { apiHost: '' } },      
      provideMockStore({
          initialState: { user: { currentUser: user } },
      }),
    ],
  });
  httpTestingController = TestBed.inject(HttpTestingController);
  httpClient = TestBed.inject(HttpClient);
  appConfig = TestBed.inject(APP_CONFIG);
});
Enter fullscreen mode Exit fullscreen mode

Unsurprisingly, the beforeEach method executes before each test runs. The first thing we do is configure the testing module. Tests run in isolation, so the setup we provide in the AppModule (or any other module) does not get applied here. So we have to configure the testing module, just like we have to configure the AppModule for our application. Let’s break this down.

We import the HttpClientTestingModule, so that all HTTP requests will hit the HttpClientTestingBackend, instead of our real API. We don’t want to make actual requests to a real API in a unit test.

imports: [HttpClientTestingModule],
Enter fullscreen mode Exit fullscreen mode

Next we provide the AuthInterceptor to our testing module and specify that it should be used in the request pipeline by providing it for the HTTP_INTERCEPTORS injection token. This means that any HTTP request (even our fake ones) will go through our interceptor. My implementation also depends on an APP_CONFIG token so I have to provide a value for that, which contains the apiHost. This is not a real API host — we just need a constant value we can compare against.

AuthInterceptor,
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },      
{ provide: APP_CONFIG, useValue: { apiHost: 'https://mockhost/api' } },
Enter fullscreen mode Exit fullscreen mode

The last thing we need to do in the providers array is call a helper method that NgRx supplies, which adds all of the providers necessary to mock the NgRx Store: provideMockStore(). If you have initial state that needs to be set for every test, you can provide that initial state to this method, as we’ve done in the code snippet above.

provideMockStore({
    initialState: { user: { currentUser: user } },
}),
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can set/modify state in the test itself, by injecting the MockStore class and calling setState(). We don’t need to do that here, but this is what that would look like:

...
store = TestBed.inject(MockStore);
...
store.setState({ user: { currentUser: user } });
Enter fullscreen mode Exit fullscreen mode

Lastly, we inject the dependencies that we need, to make them available for each test. Importing the HttpClientTestingModule is enough to make HTTP requests go to a fake backend, but the HttpTestingController class is needed to inspect those request and to return mock data, if needed.

The HttpClient is still needed and used, just like it is in the real application. The difference here is that we’ve told Angular to use a test HttpBackend, by importing the HttpClientTestingModule.

httpTestingController = TestBed.inject(HttpTestingController);
httpClient = TestBed.inject(HttpClient);
appConfig = TestBed.inject(APP_CONFIG);
Enter fullscreen mode Exit fullscreen mode

The Test

Create the following test to verify that the Authorization header will be added to any HTTP request made to the appConfig.apiHost:

it('should add auth headers for calls to appConfig.apiHost', () => {
  //arrange
  const url = `${appConfig.apiHost}/mockendpoint`;

  //act
  httpClient.get(url).subscribe();

  // assert
  const req = httpTestingController.expectOne(url);
  expect(req.request.headers.get('Authorization'))
                            .toEqual(`Bearer ${user.authToken}`);
});
Enter fullscreen mode Exit fullscreen mode

First we arrange the test, just by specifying a URL to call. The only important piece here is the appConfig.apiHost. The rest can be anything and does not actually exist anywhere.

const url = `${appConfig.apiHost}/mockendpoint`;
Enter fullscreen mode Exit fullscreen mode

Next we make the call to that URL, but remember it’s not actually going anywhere across the wire.

httpClient.get(url).subscribe();
Enter fullscreen mode Exit fullscreen mode

Finally, we get the fake request, using the HttpTestingController, and verify that the Authorization Header has been set with the user’s Bearer auth token.

const req = httpTestingController.expectOne(url);
expect(req.request.headers.get('Authorization'))
                          .toEqual(`Bearer ${user.authToken}`);
Enter fullscreen mode Exit fullscreen mode

Now we have a test to ensure that our Authorization Header will be added to every HTTP request to the appConfig.apiHost, and that the value of the Header will be ‘Bearer’ plus the user’s Bearer token.

We would also want to add a test to make sure that the opposite is true — that calls not to appConfig.apiHost do not have the Header added. I’ll include that test here for reference, but I don’t think it requires any additional explanation:

it('should not add auth headers for calls that are not to appConfig.apiHost', () => {
  //arrange
  const url = `https://someotherurl/mockendpoint`;

  //act
  httpClient.get(url).subscribe();

  // assert
  const req = httpTestingController.expectOne(url);
  expect(req.request.headers.has('Authorization')).toBe(false); 
});
Enter fullscreen mode Exit fullscreen mode

That’s it for this one. Hope you find it useful.

Bibliography

Top comments (0)