Testing Angular Services with HTTP Mocking
Unit tests for Angular services become straightforward when the HTTP boundary is treated as a dependency. Instead of calling an API, your test can capture the outgoing request, supply a controlled response and inspect exactly what the service asked for. That approach catches mistakes in URLs, payloads, headers and error handling without needing a live server, which is especially useful for Australian teams balancing local development with cloud-hosted APIs.
Angular’s HttpTestingController is built for this job. It sits behind HttpClient, records requests and lets a specification flush a response when it is ready. You are replacing the network transport with a predictable test double, rather than mocking Angular itself. Australian teams often say “give it a go” when trying a new testing pattern, but the important part here is the contract: the same method and request must produce a deterministic outcome.
This matters in markets such as Australian fintech and ecommerce, where an Angular front end often talks to a .NET or Node API while teams in Sydney, Melbourne, Brisbane and Perth ship continuously. Service tests verify the browser-side agreement; separate integration tests can still exercise the real backend. Keeping those layers distinct gives remote teams faster feedback and makes a failed check much easier to diagnose.
The examples below use Angular’s modern TestBed providers, HttpTestingController and promise-based assertions with RxJS. They cover successful requests, request matching, HTTP failures and safe fixtures. You can adapt them to an existing service, but keep each example small: a test should explain one behaviour and leave little room for unrelated setup to obscure the result.
Build an isolated testing boundary
Given a service with a method such as getCustomer(id) that calls this.http.get<Customer>(/api/customers/${id}), the test setup needs two things: the service and a mocked HTTP backend. Inject both through TestBed so Angular creates them under the same testing configuration. The service should contain the real business logic you want to verify, while the HTTP layer remains under your control.
let service: CustomerService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
CustomerService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting()
]
});
service = TestBed.inject(CustomerService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
Import provideHttpClient and withInterceptorsFromDi from @angular/common/http, and import provideHttpClientTesting and HttpTestingController from @angular/common/http/testing. Register the normal HTTP provider before the testing provider so Angular can install its interceptor chain while the testing backend takes over the final transport. In current Angular versions, these providers are preferable to the older module-based arrangement.
Keep verify() in the teardown phase. It turns pending or unmatched requests into a clear failure and prevents one specification’s traffic from affecting another. Avoid a base URL pointing to a live development API: the mocked transport should intercept relative requests before they can leave the process.
Match requests and control responses
Use expectOne to select the request you expect, then flush the response that the service should receive. A predicate is useful when the method, URL or payload matters because it documents the expected call directly in the test. Matching too loosely can accept the wrong endpoint, while matching too narrowly can make a legitimate refactor fail unnecessarily.
it('requests and returns a customer', async () => {
const result = firstValueFrom(service.getCustomer('42'));
const request = httpMock.expectOne(
req =>
req.method === 'GET' &&
req.url === '/api/customers/42'
);
request.flush({
id: '42',
name: 'Aussie customer'
});
await expectAsync(result).toBeResolvedTo({
id: '42',
name: 'Aussie customer'
});
});
After capturing a request, its underlying HttpRequest is available through request.request. That gives you access to the body, headers, response type and other request details. For a save method, for example, assert that request.request.body contains the expected customer fields. Keep structural assertions focused on the contract so a harmless field addition does not break the suite.
firstValueFrom provides a convenient way to await a single result from an observable. It also establishes the subscription before expectOne runs, which keeps the request lifecycle predictable. If a service deliberately exposes a long-lived stream, use fakeAsync with tick() or explicit subscription tracking instead. Avoid mixing asynchronous and fake asynchronous tests within the same specification, as their timing models can obscure the actual failure.
Exercise errors and edge cases
Flushing is the point at which the stubbed backend answers the service. Supply a realistic body and, where relevant, a status code, status text and headers. Successful JSON responses commonly use 200, creation responses may use 201, and a deletion endpoint may return 204. These small details help the test exercise the same branches your production code will encounter.
Failures deserve the same attention as successful responses. Calling flush with an error body and a 404 status produces an HttpErrorResponse, allowing you to verify that the service exposes or transforms the failure correctly. Import HttpErrorResponse from @angular/common/http when you need to make that assertion.
it('surfaces a failed lookup', async () => {
const result = firstValueFrom(service.getCustomer('missing'));
httpMock.expectOne('/api/customers/missing').flush(
{ message: 'Customer not found' },
{ status: 404, statusText: 'Not Found' }
);
try {
await result;
fail('Expected the lookup to fail');
} catch (error) {
expect(error).toBeInstanceOf(HttpErrorResponse);
expect((error as HttpErrorResponse).status).toBe(404);
}
});
Avoid asserting an exact status-text string when a stable numeric status or application error code is enough. Also distinguish a missing record from a connectivity failure or timeout. The HTTP mock can reproduce server errors, but it does not create genuine network delays, so test timeout policies separately with controlled timers. Unit tests can verify the browser-side contract without claiming that a live API’s authentication, CORS configuration or deployment has been proven.
Keep fixtures safe and realistic
Fixtures should resemble the API contract without carrying production baggage. Use synthetic records rather than real customer details. For Australian interfaces, include Australian dollars where appropriate and make dates unambiguous: DD/MM/YYYY is familiar locally, but ISO 8601 remains safer in API contracts and tests. That prevents a date such as 03/04/2024 from becoming 4 March in one place and 3 April in another.
Longer workflows sometimes require several responses. If a service fetches a customer and then retrieves that customer’s preferences, capture each relative URL and flush the responses in dependency order. Use a predicate when several calls share the same endpoint and you need to distinguish them by body. There is little value in recreating an entire backend inside a mock; extract complicated state transitions into separately testable logic and keep the HTTP test focused on the boundary.
Treat network isolation as non-negotiable. An absolute URL to a third-party promotional landing page must be rejected rather than fetched, because a unit test should never depend on a live external site. Relative paths such as /api/customers keep the test self-contained and make accidental network access much easier to detect.
A checklist for dependable service tests
A test earns trust when its setup, action and expected outcome are obvious. Read the specification without opening the implementation; if the intent is unclear, rename the test or simplify its fixture. Keep assertions on meaningful API details, including the method, URL, payload structure, response handling and the absence of unexpected requests.
Watch suite duration as coverage grows. The mocked backend is fast because it performs no real input or output, so repeated flushes and short waits rarely need long timers. Run the suite under the same Node and Angular versions used by your CI pipeline, and keep the test command in the project’s package scripts. For teams in Melbourne or Brisbane, that makes local feedback comparable to a hosted runner.
Use this review list when adding or changing a service test:
- Register the service through
TestBedand provide an HTTP testing backend, avoiding a hand-writtenHttpClientreplacement unless the class itself needs separate unit tests. - Match every request by method and URL, then assert headers and body details wherever the API contract requires them.
- Flush realistic success and error responses, including the status codes on which the service branches.
- Call
verify()during teardown so unmatched or pending requests fail the specification instead of leaking into another test. - Keep fixtures relative, synthetic and deterministic, and never allow a test to contact a live API or external website.
- Cover a failure path and a boundary condition, such as an empty result, malformed payload or missing required field.
The value of the suite comes from its diagnostic quality, rather than its raw number of specifications. A failure should point to the wrong endpoint, missing payload or unhandled status without requiring a debugger. That clarity helps teams ship confidently across Sydney and regional Australian offices, where reliable feedback is worth more than a complicated test harness.
Open your Angular workspace, run ng test, and add one focused service specification using this pattern. Start with an endpoint the team already understands, verify the outgoing request, supply a successful response and then add one realistic error case. Run that focused test before moving to the next endpoint.
Once it passes, bring the specification into the regular suite and share the pattern with the rest of your team. When you publish a tutorial or open-source the example, keep the API contract and fixture names consistent so other Australian developers can reproduce it. A small, dependable example gives the whole team something solid to build on.