Testing

Angular Mocking With HttpTestingController (Complete Guide)

A complete guide for testing HTTP-based services the Angular way by using HttpTestingController. Common pitfalls will be explained.

Angular Mocking With HttpTestingController (Complete Guide) — Testing article by Angular University on Angular In Depth
Angular Mocking With HttpTestingController (Complete Guide) — Testing article by Angular University on Angular In Depth
On this page · 11 sections

Most Angular applications rely on HTTP for their service layer. When it comes to testing these services, making actual network calls during tests is unreliable, sluggish, and difficult to manage.

Taking that approach would require launching a real HTTP test server, turning what should be simple unit tests into complex integration tests.

Another option involves intercepting browser APIs directly by overriding XMLHttpRequest or fetch. However, this method is brittle, and Angular already provides a far superior solution right out of the box.

Angular addresses this challenge with a complete, ready-to-use solution for testing HTTP servicesHttpTestingController.

This guide covers everything necessary to leverage this testing utility for creating clean, dependable, and fast unit tests for your HTTP-based services.

Keep in mind that all code examples use the modern Vitest syntax, not Jasmine.

Table of Contents

This post explores the following subjects:

  • Understanding Angular HTTP Mocking and its importance
  • Sample Angular HTTP Service for Testing
  • Configuring HTTP Testing with provideHttpClientTesting
  • Leveraging verify() to Catch Unexpected HTTP Requests
  • Writing Your Initial Angular HTTP Mock Test
  • Using Predicates Instead of URLs for Request Assertions
  • Testing Error Handling in HTTP Scenarios (standard vs network errors)
  • Mocking HTTP PUT and Other Modifying Requests
  • Summary and Essential Points

Let's dive straight into our detailed examination of Angular HTTP Mocking.

Understanding Angular HTTP Mocking and its importance

When writing a unit test for an Angular service that depends on HTTP, you want to avoid any actual network activity entirely.

Rather than that, your objectives are to:

  • Control the HTTP response — provide the necessary data directly from the test
  • Control timing — resolve responses synchronously within the test environment
  • Verify the request — confirm that the service generated the correct HTTP request, including URL, method, headers, and body
  • Prevent stray requests — ensure no unexpected HTTP calls occur

Angular's HttpTestingController provides all these capabilities and more.

Its mechanism involves swapping the real HTTP backend responsible for network communication with an in-memory substitute that captures requests, allowing you to respond to them manually during the test.

A crucial point to understand: HttpTestingController does not mock HttpClient itself.

Instead, it substitutes the transport layer that operates beneath it.

Your service code executes exactly as it would in a live environment — it invokes HttpClient methods, handles HttpParams, and processes responses — but no actual network requests are made.

Take note of this important detail:

To use HttpTestingController, your services must be built on top of HttpClient.

This is a strong justification for preferring HttpClient over alternatives like fetch().

Additionally, consider this:

Are you concerned that HttpClient is based on Observables, and you'd rather avoid them? We'll address that in the article.


Sample Angular HTTP Service for Testing

Below is the service that will form the basis of our test suite.

It demonstrates several standard HTTP patterns you'll frequently encounter:

  • a GET request by identifier
  • a PUT (update) operation
  • a GET request with multiple query parameters

Here's the Angular HTTP service we'll be testing:

@Injectable({
  providedIn: 'root'
})
export class CoursesService {

  private http = inject(HttpClient);

  async findCourseById(
  courseId: number): Promise<Course> {
    return firstValueFrom(
      this.http.get<Course>(
      `/api/courses/${courseId}`)
    );
  }

  async saveCourse(courseId: number, 
  changes: Partial<Course>): Promise<Course> {
    return firstValueFrom(
      this.http.put<Course>(
      `/api/courses/${courseId}`, 
      changes)
    );
  }

  async findLessons(
    courseId: number,
    filter = '',
    sortOrder = 'asc',
    pageNumber = 0,
    pageSize = 3
  ): Promise<Lesson[]> {
    const params = new HttpParams()
      .set('courseId', courseId.toString())
      .set('filter', filter)
      .set('sortOrder', sortOrder)
      .set('pageNumber', pageNumber.toString())
      .set('pageSize', pageSize.toString());

    const res = await firstValueFrom(
      this.http.get<{ 
      payload: Lesson[] 
      }>(`/api/lessons`, { params })
    );
    return res.payload;
  }
}

Before we start writing tests, let's examine a few characteristics of this service:

  • it depends on HttpClient, a necessary condition for using HttpTestingController
  • HttpClient is Observable-based, but we aim for our service layer to be Promise-based, aligning with the async/await syntax for a more straightforward, almost synchronous developer experience.
  • We use firstValueFrom(), a standard Angular RxJS interop helper, to transform Observables into Promises.
  • Using HttpClient is still a wise choice even without Observables, due to two key benefits: straightforward request mocking in tests and access to Angular's HTTP interceptors.
  • Everything else in this discussion about HttpTestingController is not affected by the service's API style, whether Promises or Observables work fine, as long as the service internally relies on HttpClient.

Configuring HTTP Testing with provideHttpClientTesting

Let's set up a test suite for the CoursesService:

describe('CoursesService', () => {
  let service: CoursesService;
  let httpTesting: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });

    service = TestBed.inject(CoursesService);
    httpTesting = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpTesting.verify();
  });

});

Here's a breakdown of what we've done:

  • We create a small Angular runtime environment before each test, containing only an instance of CoursesService and its immediate dependencies.
  • Notice that we provide a real HttpClient instance via provideHttpClient(). We are not mocking it.
  • Then, we add provideHttpClientTesting(). This function swaps the default XMLHttpRequest-based transport used by HttpClient with a mock implementation.

That completes our test suite configuration.

But what is the purpose of the `verify()` call placed there?

Leveraging verify() to Catch Unexpected HTTP Requests

Concerning the afterEach block in our test configuration:

afterEach(() => {
  httpTesting.verify();
});

The purpose of verify() is to ensure that no HTTP requests were issued during the test that were not explicitly handled and accounted for.

As we'll see, a core activity in our HTTP tests is to use HttpTestingController to assert that particular HTTP requests were made, checking their URL, body, parameters, headers, and so on.

If any requests remain unverified after the test completes, it could indicate a bug in the service.

Imagine a scenario where a service method inadvertently calls two endpoints instead of one because of a defect.

Without verify(), your test would pass as long as the specific request you're checking is found. The additional unintended request would still have been sent.

This would allow the bug to go undetected.

With verify() in place, however, that extra request is identified, causing the test to fail.

Ensure verify() is placed in afterEach, rather than within individual tests. This prevents you from accidentally omitting it for any single test.

Writing Your Initial Angular HTTP Mock Test

We'll begin by testing the findCourseById method, which is a basic HTTP GET request.

We'll use the following mock data:

const MOCK_COURSE: Course = {
    id: 12,
    title: 'Angular For Beginners',
    description: 'Learn Angular from scratch.',
    iconUrl: 'https://example.com/icon.png',
    category: 'BEGINNER',
    lessonsCount: 10
  };

Here is the complete test annotated, followed by a detailed breakdown:

it('should retrieve a course by id', async () => {
    
  // 1. Trigger the HTTP request
  // do NOT "await" yet, as that would hang 
  // and timeout the test
  const coursePromise = service.findCourseById(12);

  // 2. Assert that exactly one request
  // was made to this exact URL
  const req = httpTesting.expectOne('/api/courses/12');

  // 3. Assert request method
  expect(req.request.method).toBe('GET');

  // 4. Trigger response to GET with mock data
  req.flush(MOCK_COURSE);

  // 5. Only now await the resolved value
  const course = await coursePromise;

  expect(course.id).toBe(12);
  expect(course.title).toBe('Angular For Beginners');
});

Let's go through this step by step:

Step 1 — initiate the request without await. The call service.findCourseById(12) initiates the HTTP operation, resulting in a pending Promise.

At this point, the request is held in the testing controller's queue, awaiting validation and a response.

Using await here would lead to a test that hangs indefinitely, as the Promise can only be resolved after we supply a mock response.

And providing that response requires the code execution to proceed past the service call, as we will see.

Step 2 — expectOne finds the pending request based on its URL, ensuring that exactly one request matches that specific URL.

The test will throw an error right away if there are zero or multiple matching requests. Thus, this statement serves as an assertion in itself.

Step 3 — req.request provides access to the outgoing request before you send a response. This is where we verify that the method is indeed GET.

Step 4 — flush delivers the response data. The testing backend resolves the Observable, which in turn resolves the Promise via firstValueFrom.

Step 5 - It's now safe to use await, since the response has already been provided. The Promise is already resolved, so await immediately returns the course object.

The test would have stalled if we had placed await on that promise at any time before calling flush.

Using Predicates Instead of URLs for Request Assertions

Alternatively, expectOne can accept a filter function to match a request more flexibly, rather than just a simple string.

Let's test the findLessons() method, using expectOne with a filter function:

it('should find lessons by query', async () => {

    const promise = service.findLessons(
    12, 'filter-text', 'desc', 2, 10);
    
    const req = httpTesting.expectOne(
        req => req.url === '/api/lessons');
    
    const params = req.request.params;
    
    expect(params.get('courseId')).toBe('12');
    expect(params.get('filter')).toBe('filter-text');
    expect(params.get('sortOrder')).toBe('desc');
    expect(params.get('pageNumber')).toBe('2');
    expect(params.get('pageSize')).toBe('10');
    
    const mockLessons = {
    payload: [{
           id: 12, 
           description: "Lesson 1"
        }]
    };
    
    req.flush(mockLessons);
    
    const result = await promise;
    
    expect(result).toBe(mockLessons.payload);
})

This filter approach is particularly useful in scenarios like this one, where the full URL is complicated due to multiple query parameters.

Remember that, besides filtering on the URL, you can also filter based on request headers and body.

Next, we'll discuss how to test different kinds of error situations.

Angular Testing In Depth Course

If you appreciate the teaching style in this article, explore the free sample video lessons from the Angular Testing In Depth (Signals Edition) course:

Angular Mocking With HttpTestingController (Complete Guide) — figure 1

Let's now get back to the main topic.

Testing Error Handling in HTTP Scenarios

We can also leverage HttpTestingController to emulate error responses by explicitly setting an HTTP error status code.

Here's how to simulate a standard 404 Not Found response:


it('should reject if the server returns 404', async () => {
    const coursePromise = service.findCourseById(999);
    
    const req = httpTesting.expectOne(
        '/api/courses/999');
    req.flush('Course not found', {
      status: 404,
      statusText: 'Not Found'
    });

    return expect(coursePromise).rejects.toThrow();
});

Note that this last test represents a case where the server did send back a response.

We can also emulate more severe situations like a network failure:

it('should handle network error ', async () => {
    
    const coursePromise = service.findCourseById(1);

    const req = httpTesting.expectOne('/api/courses/1');

    req.error(new ProgressEvent('network error'));

    return expect(coursePromise).rejects.toThrow();
})

Notice that req.error() expects a ProgressEvent — this is the same fundamental browser event that XMLHttpRequest emits when a connection drops, DNS lookup fails, or a CORS preflight is denied.

In this case, there is no HTTP response whatsoever; the request simply never reaches completion.

Mocking HTTP PUT and Other Modifying Requests

The same principles used for GET requests also apply when testing PUT operations.

The core difference lies in the HTTP method, but the overall testing strategy remains unchanged.

To be thorough, here's how to test the saveCourse() method:

describe('saveCourse', () => {

  it('should save the course changes', async () => {
    const changes: Partial<Course> = { 
        title: 'Angular Advanced Course' 
    };

    const savePromise = service.saveCourse(12, changes);

    const req = httpTesting.expectOne(
       '/api/courses/12');

    expect(req.request.method).toBe('PUT');
    expect(req.request.body).toEqual(changes);

    const updatedCourse: Course = {
      id: 12,
      title: 'Angular Advanced Course',
      description: 'Advanced Angular patterns.',
      iconUrl: 'https://example.com/icon.png',
      category: 'ADVANCED',
      lessonsCount: 20
    };

    req.flush(updatedCourse);

    const saved = await savePromise;
    expect(saved.title).toBe('Angular Advanced Course');
  });

});

With this final example, we have tested all the methods of CoursesService.

Let's now summarize the main concepts we've covered.

Summary and Essential Points

HttpTestingController offers a practical, Angular-native method for testing HTTP services without needing a real server or modifying browser globals.

It reinforces the argument for using Angular's HttpClient in your services, even when you're not building an Observable-based service layer.

Here are the key points to remember:

The critical rule: never `await` a service call before using flush. The proper sequence is: call → expectOne → flush → await.

Consistently call verify() in afterEach. It acts as a safeguard against unintended requests that might otherwise be missed.

When necessary, use a filter function with expectOne. At times, a filter is a more convenient way to find a specific request than knowing the complete URL.

flush can emulate any HTTP response — including errors like 4xx and 5xx. For network-level problems, use error().

By adopting these strategies, you ensure your service layer tests are fast, reliable, maintainable, and genuinely helpful.

Thank you for reading this guide. If you'd like to receive future articles directly in your email, be sure to subscribe to my newsletter:

AU
Angular University

Writes about RxJS, Components, Signals. Active 2015–2026.

All 79 articles →