Skip to content

Angular Service Testing with Karma

Angular services are usually simpler to test than components, since there's typically no template or DOM rendering involved — just dependency injection and, often, HTTP interactions to mock. This lesson covers both.

Testing a Simple Service

For a service with no external dependencies, TestBed.inject() resolves an instance from the testing module exactly as Angular's real dependency injector would, letting you call its methods and assert on the results directly.

import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';

describe('CalculatorService', () => {
  let service: CalculatorService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CalculatorService);
  });

  it('adds two numbers', () => {
    expect(service.add(2, 3)).toBe(5);
  });
});

No declarations or providers are needed here since CalculatorService has no dependencies of its own to resolve.

Mocking HTTP with HttpClientTestingModule

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

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

  it('fetches users from the API', () => {
    let result: any[] = [];
    service.getUsers().subscribe((users) => (result = users));

    const req = httpMock.expectOne('/api/users');
    req.flush([{ id: 1, name: 'Ada' }]);

    expect(result).toEqual([{ id: 1, name: 'Ada' }]);
  });
});
  • HttpClientTestingModule replaces Angular's real HttpClient backend with a controllable mock.
  • HttpTestingController.expectOne() asserts exactly one matching request was made, and returns it for you to control.
  • req.flush(data) simulates a successful response with the given body.
  • httpMock.verify() in afterEach ensures no unexpected/unhandled requests were left outstanding.

Service Testing Cheat Sheet

Key APIs for testing services with and without HTTP dependencies.

Task Approach
Resolve a service instance TestBed.inject(MyService)
Mock HTTP backend Import HttpClientTestingModule
Assert a specific request was made httpMock.expectOne(url)
Simulate a successful response req.flush(data)
Simulate an error response req.flush(null, { status: 500, statusText: 'Server Error' })
Verify no unexpected requests remain httpMock.verify() in afterEach

Testing HTTP Error Handling

HttpTestingController can simulate failed requests just as easily as successful ones, which is essential for verifying a service's error-handling logic actually works as intended.

it('handles a failed request', () => {
  let errorCaught: any;
  service.getUsers().subscribe({
    error: (err) => (errorCaught = err)
  });

  const req = httpMock.expectOne('/api/users');
  req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });

  expect(errorCaught.status).toBe(500);
});

Mocking Non-HTTP Service Dependencies

For services depending on other services (rather than HTTP directly), the same providers + spy object pattern from component testing applies identically here.

const loggerSpy = jasmine.createSpyObj('LoggerService', ['log']);

TestBed.configureTestingModule({
  providers: [
    UserService,
    { provide: LoggerService, useValue: loggerSpy }
  ]
});

Common Mistakes

  • Forgetting httpMock.verify() in afterEach, silently missing unexpected or duplicate HTTP requests.
  • Testing against a real backend instead of HttpClientTestingModule, making tests slow and network-dependent.
  • Not testing error-handling paths at all, only ever simulating successful responses.
  • Providing a real, heavy dependency service when a lightweight spy object would isolate the test better.

Key Takeaways

  • TestBed.inject() resolves a service instance exactly as Angular's real injector would.
  • HttpClientTestingModule and HttpTestingController provide full control over mocked HTTP requests.
  • req.flush() simulates both successful and error responses for thorough error-handling coverage.
  • httpMock.verify() catches unexpected outstanding requests a test might otherwise miss silently.

Pro Tip

Always write at least one error-path test alongside every happy-path HTTP service test. HttpTestingController.flush() makes simulating a 500 or 404 nearly as easy as a success response, so there's little excuse to skip it.