Angular - Test Service With HttpClient (With Code Examples)


How to test Angular services with the HttpClient.

Want to skip the read and get to the code right away? Then click here.

Have you ever noticed...

That...

A common dependency inside an Angular service file is the HttpClient? ๐Ÿ˜ธ

Maybe your experience with Angular has been different than mine but I find that the HttpClient is a common dependency in an Angular service.

And rightly so.

thats-how-you-do-it

It's considered best practice to put your data logic inside a service that can be shared across the application.

This leaves the job of displaying or rendering the data to your front-end components. Whereas your service is responsible for retrieving that data from whatever your source happens to be.

But just how do you test a service that depends on the HttpClient?

Once upon a time there was a young Angular developer named Harry.

This developer had learned some basic things about the Angular framework and was given the job of building a shinny, new Angular application for his company.

Harry began coding and doing the best he knew how. He followed all the best practices he read about on the Angular website. He always double-checked to make sure that the HttpClient was never injected in any of his components, leaving the API calls for a service that he then injected into his Angular components.

Things were going well...

Until...

His boss showed up one day and told him that before the new app could hit production he needed to write tests for all the public methods in his services. ๐Ÿ™ˆ ๐Ÿ™ˆ ๐Ÿ™ˆ

"No problem", Harry said, and he went back to his computer and began writing tests.

He had written a dozen tests when he came to a service that depended on the HttpClient module. Confused, he sat down to figure this out and try to decide how to best test this Angular service that depended on the HttpClient module.

First he tried to inject the HttpClient module but that threw a weird injection error.

So he went to Google to try to figure this out and instead of finding the clarification he needed, his head was soon with questions.

Should I mock the HTTP Client or have it call a real API server?

If I have it call a real API server, then I'll first have to authenticate with it, and how would I do that?

How do I mock Angular's HTTP Client without getting an injection error?

Maybe I should just skip the tests for this Angular service? I think my boss won't find out.

So, how should Harry test his Angular service?

Should he use a unit test? Or an integration test?

And that, my friend, is what this complete guide to testing Angular HTTP services is for.

I'm going to show you and Harry how to properly test your Angular HTTP services and help shake those nasty bugs out of your code.

Should you mock the Angular HttpClient? Or call a real API service?

When you write tests for your Angular service, should you mock the HTTP calls to your data service (API server)?

Or should you actually call a real server?

Well... it all depends on what you want to accomplish by writing tests for your HTTP service.

Sometimes it's a good idea to call a real API server when writing tests for your HTTP services. But more times then not, those tests are brittle integration that snap, pop and blow up in your face. They break faster and sooner than unit tests.

So instead of making real HTTP calls in your Angular tests, most of the time you should mock the HTTP calls. Or in other words, write unit tests instead of integration tests when testing your data (API) services.

Also, when done properly, unit tests are faster to write and give a higher ROI then integration tests which is why I recommend mocking the Angular HTTP client when testing your data services.

So, now that we've decided to mock the HTTP client, how do we test an Angular service that depends on the HttpClient module?

Write your first unit test for an Angular service

Here's the quickest way to write a unit test for an Angular service that depends on HttpClient.

First, install jasmine-auto-spies.

npm i --include=dev jasmine-auto-spies

And then, write the test!

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { Customer } from '../models/customer';

import { CustomersService } from './customers.service';

describe('CustomersService', () => {
  let service: CustomersService;
  let httpSpy: Spy<HttpClient>;
  let fakeCustomers: Customer[] = [
    {
      id: "1",
      name: "Fake Customer",
      email: "fake@fake.com"
    },
    {
      id: "2",
      name: "Fake Customer Two",
      email: "fake-two@fake.com"
    }
  ];

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        CustomersService,
        { provide: HttpClient, useValue: createSpyFromClass(HttpClient) }
      ]
    });

    service = TestBed.inject(CustomersService);
    httpSpy = TestBed.inject<any>(HttpClient);
  });

  it('should return an expected list of customers', (done: DoneFn) => {
    httpSpy.get.and.nextWith(fakeCustomers);

    service.getAllCustomers().subscribe(
      customers => {
        expect(customers).toHaveSize(fakeCustomers.length);
        done();
      },
      done.fail
    );
    expect(httpSpy.get.calls.count()).toBe(1);
  });
});

So, what did we just do?

The test above is from a simple demo project that attempts to simulate a real-world application.

In our beforeEach function we injected the HttpClient as a spy using the handy-dandy jasmine-auto-spies library.

Then, inside the it function, we mocked the HTTP call instead of calling a real server. And then we ran our tests to make sure the getAllCustomers function returns the desired result.

Writing tests for a POST request

So, what if you have a POST request that you need to write tests for?

Again, use jasmine-auto-spies to create mock an HTTP POST request.

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { Customer } from '../models/customer';

import { CustomersService } from './customers.service';

describe('CustomersService', () => {
  let service: CustomersService;
  let httpSpy: Spy<HttpClient>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        CustomersService,
        { provide: HttpClient, useValue: createSpyFromClass(HttpClient) }
      ]
    });

    service = TestBed.inject(CustomersService);
    httpSpy = TestBed.inject<any>(HttpClient);
  });

  it('should create a new customer', (done: DoneFn) => {

    var newCustomer = {
      name: "New Customer",
      email: "new@customer.com"
    } as Customer;

    httpSpy.post.and.nextWith(newCustomer);

    service.createCustomer(newCustomer).subscribe(
      customer => {
        expect(customer).toEqual(newCustomer);
        done();
      },
      done.fail
    );
    expect(httpSpy.post.calls.count()).toBe(1);
  });
});

Writing tests for a PUT request

What about writing a test for a PUT request?

It's the same as the examples above, except we're swapping POST for PUT.

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { Customer } from '../models/customer';

import { CustomersService } from './customers.service';

describe('CustomersService', () => {
  let service: CustomersService;
  let httpSpy: Spy<HttpClient>;
  let fakeCustomers: Customer[] = [
    {
      id: "1",
      name: "Fake Customer",
      email: "fake@fake.com"
    },
    {
      id: "2",
      name: "Fake Customer Two",
      email: "fake-two@fake.com"
    }
  ];

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        CustomersService,
        { provide: HttpClient, useValue: createSpyFromClass(HttpClient) }
      ]
    });

    service = TestBed.inject(CustomersService);
    httpSpy = TestBed.inject<any>(HttpClient);
  });

  it('should update a customer with given customer id', (done: DoneFn) => {

    var customer = fakeCustomers[0];
    customer.name = "Updated Customer";

    httpSpy.put.and.nextWith(customer);

    service.updateCustomer(customer).subscribe(
      customer => {
        expect(customer.name).toEqual("Updated Customer");
        done();
      },
      done.fail
    );
    expect(httpSpy.put.calls.count()).toBe(1);
  });
});

Writing tests for a DELETE request

import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { Customer } from '../models/customer';

import { CustomersService } from './customers.service';

describe('CustomersService', () => {
  let service: CustomersService;
  let httpSpy: Spy<HttpClient>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        CustomersService,
        { provide: HttpClient, useValue: createSpyFromClass(HttpClient) }
      ]
    });

    service = TestBed.inject(CustomersService);
    httpSpy = TestBed.inject<any>(HttpClient);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should delete an existing customer', (done: DoneFn) => {

    httpSpy.delete.and.nextWith(new HttpResponse ({
      status: 200
    }));

    service.deleteCustomer("1").subscribe(
      response => {
        expect(response.status).toEqual(200);
        done();
      },
      done.fail
    );
    expect(httpSpy.delete.calls.count()).toBe(1);
  });
});

Writing tests for a 404 error

And then, of course, if you're a smart developer you'll want to test for the unexpected errors.

So, how do we test for an HTTP error? Like a 404?

This example was created to mock a 404 response but you can modify it to fit your wants and needs.

import { HttpClient, HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { Customer } from '../models/customer';

import { CustomersService } from './customers.service';

describe('CustomersService', () => {
  let service: CustomersService;
  let httpSpy: Spy<HttpClient>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        CustomersService,
        { provide: HttpClient, useValue: createSpyFromClass(HttpClient) }
      ]
    });

    service = TestBed.inject(CustomersService);
    httpSpy = TestBed.inject<any>(HttpClient);
  });

  it('should return a 404', (done: DoneFn) => {

    var customerId = "89776683";

    httpSpy.get.and.throwWith(new HttpErrorResponse({
          error: "404 - Not Found",
          status: 404
    }));

    service.getCustomer(customerId).subscribe(
      customer => {
        done.fail("Expected a 404");
      },
      error => {
        expect(error.status).toEqual(404);
        done();
      }
    );
    expect(httpSpy.get.calls.count()).toBe(1);
  });
});

Conclusion

And that, my friend, is the complete guide on how to test Angular HTTP services.

By now you and your friend Harry know how to write simple and fasts tests for his services. And Harry is already back at it again. His project has a great code coverage score and is almost ready to deploy his new Angular application.

If you want to see the entire code for this sample project, you can find it here.

Questions? Comments? Don't hesitate to reach out.

signature

Angular Consultant

P.S. - In case youโ€™re one of the people (like me) who skim to the bottom before you read the page, hereโ€™s what this is about:

  • Should you mock the HttpClient or call a real API server?
  • The fast way to test an Angular HTTP service
  • Complete Angular testing examples for the HttpClient GET, POST, PUT or DELETE requests.
  • How to mock and test an HTTP response code