Ch. 4 · Angular

Angular HTTP Request Contract Tests

Use Angular HTTP testing to verify request URLs, methods, response mapping and failures without arbitrary delays.

~3 min readintermediateupdated Oct 3, 2026

A service test should establish what an HTTP operation asks for and how its outcome reaches callers. Returning a canned object from a fake service misses request construction. Angular’s HTTP testing backend lets a test inspect and complete the request itself.

Before you start

Know dependency injection, HttpClient and observables. The example uses a test runner providing describe, it, expect, beforeEach and afterEach globals. Install it inside an Angular test project; it is not a standalone Node.js script. The application-specific service below owns one GET operation.

Step-by-step walkthrough

Step 1: Replace transport at the right boundary

Configure HttpClient and then its testing provider. The test should exercise the real service method and HTTP request construction while preventing external traffic. Replacing CatalogService itself would test the replacement rather than the service’s contract.

Step 2: Subscribe before expecting a request

The test activates the returned observable, then asks the testing controller for the matching request. Check method and URL independently of the supplied response. If nobody subscribes, a normal cold HTTP observable has not started its operation.

Step 3: Complete or fail deliberately

Flush a controlled payload to verify mapping and caller delivery. For an HTTP failure, provide an error status and assert the intended error outcome. Verification after each test detects unexpected outstanding requests; it does not substitute for assertions about the meaningful response.

Worked scenario

import { Injectable } from '@angular/core';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting }
  from '@angular/common/http/testing';
@Injectable()
class CatalogService {
  constructor(private http: HttpClient) {}
  load() { return this.http.get<{ names: string[] }>('/api/catalog'); }
}
describe('CatalogService', () => {
  beforeEach(() => TestBed.configureTestingModule({
    providers: [CatalogService, provideHttpClient(), provideHttpClientTesting()]
  }));
  afterEach(() => TestBed.inject(HttpTestingController).verify());
  it('requests and returns the catalog', () => {
    let names: string[] | undefined;
    TestBed.inject(CatalogService).load().subscribe(result => {
      names = result.names;
    });
    const request = TestBed.inject(HttpTestingController)
      .expectOne('/api/catalog');
    expect(request.request.method).toBe('GET');
    request.flush({ names: ['Angular', 'React'] });
    expect(names).toEqual(['Angular', 'React']);
  });
});
TypeScript

The callback has not produced names before flush. After the controlled response, names contains the supplied result. A wrong URL prevents expectOne from matching; a wrong method fails its explicit assertion. This catches a different class of regression from testing a fake service that always returns the desired names.

Common mistake

An HTTP generic type does not validate the runtime response shape. This test proves delivery for the selected fixture, not that every backend response is safe. If the service parses unknown external data, add malformed-response tests at that parsing boundary. Also avoid real-time sleeps: request inspection and flushing already control completion.

Verify the behavior

Add a 500 response using flush with status and statusText, subscribe with an error handler and verify the documented failure. Test query parameters through request.params rather than relying on arbitrary parameter ordering. If an interceptor retries, explicitly inspect and complete the subsequent request; otherwise a retry may remain outstanding.

Interview exercise

Why keep a real backend contract test if this local test passes?

Answer and reasoning

The testing backend verifies local request construction and controlled outcomes. It cannot prove the deployed server’s routing, authentication or payload remains compatible. A representative integration or contract check supplies that additional evidence. Keeping the boundaries explicit lets fast local tests remain deterministic without overstating what they established.

Continue learning

See Angular component testing and Angular interview questions. Follow the official HTTP testing guide for provider and controller behavior.

More in Angular

read ✓Angular · mid

Angular Defer Blocks and Lazy Rendering

Load template regions on demand with @defer, choose a trigger, and use placeholders without delaying critical content.

~2 min readread →
read ✓Angular · hard

Angular Global Error Handling

Catch uncaught errors with a custom ErrorHandler, distinguish client from HTTP errors, and surface safe messages to users.

~2 min readread →
read ✓Angular · mid

Angular Host Directives and Composition

Attach behavior to elements with attribute directives, bind to the host with the host property, and compose directives with hostDirectives.

~2 min readread →
esc