Skip to content

Angular Unit Testing

Unit tests verify a single piece of logic in isolation, usually a service, pipe, or validator, without rendering any template. This lesson covers writing them effectively in Angular.

What Makes a Good Unit Test?

A good unit test exercises one unit of behavior (a method, a function) with a specific input, and asserts a specific, predictable output or side effect, independent of the rest of the application.

Services, pipes, and validator functions are the most natural fit for unit testing in Angular, since they're plain classes/functions with no template to render.

describe('CartService', () => {
  it('calculates the total price of items in the cart', () => {
    const cart = new CartService();
    cart.add({ id: '1', price: 10, qty: 2 });
    cart.add({ id: '2', price: 5, qty: 1 });

    expect(cart.total()).toBe(25);
  });
});

No TestBed, no Angular test module setup needed here, CartService is instantiated directly like any plain class.

The Arrange-Act-Assert Pattern

it('description of expected behavior', () => {
  // Arrange: set up the object/data under test
  const service = new MyService();

  // Act: perform the behavior being tested
  const result = service.doSomething(input);

  // Assert: verify the outcome
  expect(result).toBe(expectedValue);
});
  • describe() groups related tests; it() (or test()) defines an individual test case.
  • Arrange-Act-Assert keeps each test readable: set up, perform the action, then check the result.
  • expect(value).toBe(...)/toEqual(...)/toThrow(...) are common Jasmine/Jest matchers.
  • For services with dependencies, use TestBed to configure a testing module with mocked providers.

Unit Testing Cheatsheet

Common patterns for unit testing Angular building blocks.

What You're Testing Approach
A service with no dependencies new MyService() directly, no TestBed needed
A service with injected dependencies TestBed.configureTestingModule({ providers: [...] })
A pipe new MyPipe().transform(input) directly
A validator function Call it directly with a new FormControl(value)
Mocking a dependency { provide: RealService, useValue: fakeService }
Async code fakeAsync/tick() or returning a Promise/Observable-aware test

Testing a Service With Injected Dependencies

When a service depends on another service (like HttpClient), use TestBed to configure a minimal testing module, substituting real dependencies with mocks so the test stays fast and isolated from real network calls.

describe('UserService', () => {
  let service: UserService;
  let httpMock: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpMock = jasmine.createSpyObj('HttpClient', ['get']);
    TestBed.configureTestingModule({
      providers: [UserService, { provide: HttpClient, useValue: httpMock }],
    });
    service = TestBed.inject(UserService);
  });

  it('fetches a user by id', () => {
    httpMock.get.and.returnValue(of({ id: '1', name: 'Ada' }));
    service.getUser('1').subscribe(user => {
      expect(user.name).toBe('Ada');
    });
  });
});

Testing HttpClient Calls With HttpTestingController

Rather than manually mocking HttpClient, Angular provides HttpTestingController, which intercepts real requests made through the testing module's HttpClient and lets you assert on them and control their responses directly.

TestBed.configureTestingModule({
  providers: [UserService, provideHttpClient(), provideHttpClientTesting()],
});
const httpMock = TestBed.inject(HttpTestingController);
const service = TestBed.inject(UserService);

service.getUser('1').subscribe();

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

Testing Asynchronous Code

Angular's fakeAsync/tick() utilities let you control simulated time in a test, useful for testing debounced logic, timers, or Promise-based flows synchronously and deterministically.

it('debounces search input', fakeAsync(() => {
  const spy = jasmine.createSpy('search');
  const search$ = new Subject<string>();
  search$.pipe(debounceTime(300)).subscribe(spy);

  search$.next('a');
  tick(100);
  search$.next('ab');
  tick(300);

  expect(spy).toHaveBeenCalledWith('ab');
  expect(spy).toHaveBeenCalledTimes(1);
}));

Common Mistakes

  • Reaching for TestBed and full Angular test module setup for a plain class/function that doesn't need it.
  • Testing against a real backend or real HttpClient instead of mocking it with HttpTestingController.
  • Writing tests that depend on execution order or shared mutable state between test cases.
  • Not calling httpMock.verify(), silently missing unexpected or unhandled HTTP requests in a test.

Key Takeaways

  • Unit tests verify one isolated piece of logic, typically a service, pipe, or validator function.
  • Plain classes/functions with no Angular dependencies can be tested with new directly, no TestBed required.
  • TestBed and HttpTestingController support testing services with real dependency injection and HTTP calls.
  • fakeAsync/tick() let you test timing-dependent logic (debouncing, timers) deterministically.

Pro Tip

Use HttpTestingController instead of manually mocking HttpClient with spies whenever possible, it verifies the exact URL, method, and body your service actually sends, catching subtle request-shape bugs that a loose mock would miss.