Testing in Angular

Testing is the process of verifying that application code behaves as expected. Writing tests is not just a quality assurance practice — it is a design discipline that leads to better, more modular code. Angular is built with testability in mind, and comes with all the tools needed to write tests without additional configuration.

Angular supports two primary types of testing:

  • Unit Tests — Test individual functions, classes, or components in isolation.
  • End-to-End (E2E) Tests — Test the entire application by simulating real user interactions in a browser.

Tools Included with Angular

Jasmine

Jasmine is the testing framework used to write test cases. It provides functions for describing tests (describe), writing individual test cases (it), and making assertions (expect).

Karma

Karma is a test runner that executes the Jasmine tests in a real browser (Chrome by default) and reports the results in the terminal. Running ng test starts Karma.

TestBed

TestBed is Angular's testing utility. It creates a minimal Angular testing environment (a test module) where components, services, and other Angular features can be tested with full Angular behavior — including dependency injection, templates, and change detection.

Running Tests


ng test          ← Run all tests and watch for changes
ng test --once   ← Run all tests once and exit (for CI pipelines)
ng test --code-coverage  ← Generate a code coverage report

Writing Unit Tests for a Service

Services are the easiest to test because they have no template — only TypeScript logic. Testing a service directly without TestBed is often the simplest approach:


// calculator.service.ts
import { Injectable } from '@angular/core';

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

  add(a: number, b: number): number {
    return a + b;
  }

  subtract(a: number, b: number): number {
    return a - b;
  }

  multiply(a: number, b: number): number {
    return a * b;
  }

  divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error('Cannot divide by zero');
    }
    return a / b;
  }
}

// calculator.service.spec.ts
import { CalculatorService } from './calculator.service';

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

  beforeEach(() => {
    service = new CalculatorService();   // Create a fresh instance before each test
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should add two numbers correctly', () => {
    const result = service.add(5, 3);
    expect(result).toBe(8);
  });

  it('should subtract two numbers correctly', () => {
    const result = service.subtract(10, 4);
    expect(result).toBe(6);
  });

  it('should multiply two numbers correctly', () => {
    expect(service.multiply(3, 7)).toBe(21);
  });

  it('should divide two numbers correctly', () => {
    expect(service.divide(20, 4)).toBe(5);
  });

  it('should throw an error when dividing by zero', () => {
    expect(() => service.divide(10, 0)).toThrowError('Cannot divide by zero');
  });

  it('should return negative number for negative subtraction', () => {
    expect(service.subtract(3, 10)).toBe(-7);
  });
});

Test Structure Explained

describe()

Groups related tests under a label. Usually named after the class or feature being tested.

it()

Defines a single test case. The description should read as a sentence: "should add two numbers correctly."

beforeEach()

Runs setup code before each test. This ensures each test starts with a clean, fresh state.

expect()

Makes an assertion — verifies that a value meets a condition. Common matchers include toBe(), toEqual(), toBeTruthy(), toBeFalsy(), toContain(), toThrowError().

Testing a Service with HTTP Requests

When testing services that use HttpClient, Angular provides HttpClientTestingModule to intercept and control HTTP requests without making real network calls:


// post.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { PostService } from './post.service';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],   // Use the test version of HttpClient
      providers: [PostService]
    });

    service = TestBed.inject(PostService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();   // Verify no unexpected HTTP requests were made
  });

  it('should fetch all posts', () => {
    const mockPosts = [
      { id: 1, title: 'First Post', body: 'Content', userId: 1 },
      { id: 2, title: 'Second Post', body: 'Content', userId: 1 }
    ];

    service.getAllPosts().subscribe(posts => {
      expect(posts.length).toBe(2);
      expect(posts[0].title).toBe('First Post');
    });

    // Intercept the HTTP request and return mock data
    const req = httpMock.expectOne('https://jsonplaceholder.typicode.com/posts');
    expect(req.request.method).toBe('GET');
    req.flush(mockPosts);   // Respond with mock data
  });
});

Testing Components with TestBed

Component testing uses TestBed to create a minimal Angular environment that includes the component, its template, and its dependencies:


// counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CounterComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();   // Trigger initial change detection
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should start with count = 0', () => {
    expect(component.count).toBe(0);
  });

  it('should increment count when increment() is called', () => {
    component.increment();
    expect(component.count).toBe(1);

    component.increment();
    expect(component.count).toBe(2);
  });

  it('should decrement count when decrement() is called', () => {
    component.count = 5;
    component.decrement();
    expect(component.count).toBe(4);
  });

  it('should display the count in the template', () => {
    component.count = 7;
    fixture.detectChanges();   // Update the DOM after changing data

    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('h2')?.textContent).toContain('7');
  });

  it('should increment when the + button is clicked', () => {
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    fixture.detectChanges();

    expect(component.count).toBe(1);
  });
});

Key Testing Utilities

ComponentFixture

A wrapper around the component that provides access to the component instance, the native DOM element, and change detection control.

fixture.detectChanges()

Manually triggers Angular's change detection. Call this after changing data to update the DOM, or when interacting with the component and needing the template to reflect changes.

fixture.nativeElement

The underlying DOM element of the component. Use standard DOM methods like querySelector() to find elements and test their content.

Mocking Dependencies

When testing a component that uses a service, replace the real service with a mock to avoid testing both the component and the service simultaneously:


// products.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
import { ProductService } from '../services/product.service';

// Create a mock version of the service
const mockProductService = {
  getAllProducts: jasmine.createSpy('getAllProducts').and.returnValue([
    { id: 1, name: 'Test Product', price: 29.99 }
  ])
};

describe('ProductsComponent', () => {
  let component: ProductsComponent;
  let fixture: ComponentFixture<ProductsComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ProductsComponent],
      providers: [
        { provide: ProductService, useValue: mockProductService }  // Inject mock
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ProductsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should load products from the service on init', () => {
    expect(mockProductService.getAllProducts).toHaveBeenCalled();
    expect(component.products.length).toBe(1);
    expect(component.products[0].name).toBe('Test Product');
  });
});

Common Jasmine Matchers Reference


expect(value).toBe(expected)            ← Strict equality (===)
expect(value).toEqual(expected)         ← Deep equality (objects/arrays)
expect(value).toBeTruthy()              ← Any truthy value
expect(value).toBeFalsy()               ← Any falsy value (null, 0, '', false)
expect(value).toBeNull()                ← Strictly null
expect(value).toBeUndefined()           ← Strictly undefined
expect(value).toContain(item)           ← String/array contains item
expect(value).toBeGreaterThan(n)        ← Number comparison
expect(value).toBeLessThan(n)           ← Number comparison
expect(fn).toThrowError(msg)            ← Function throws an error
expect(spy).toHaveBeenCalled()          ← Spy was called
expect(spy).toHaveBeenCalledWith(args)  ← Spy was called with specific arguments
expect(spy).toHaveBeenCalledTimes(n)    ← Spy was called n times

Summary

Angular testing uses Jasmine for writing tests and Karma for running them in a browser. ng test starts the test runner. Unit tests for services are written using describe, it, beforeEach, and expect. Services that make HTTP requests are tested with HttpClientTestingModule and HttpTestingController to intercept and mock network requests. Component tests use TestBed to create a testing module, ComponentFixture to access the component and its DOM, and fixture.detectChanges() to trigger rendering. Dependencies like services are replaced with mock objects using providers in the test module configuration. Testing in isolation — one unit at a time — leads to reliable, meaningful test results.

Leave a Comment

Your email address will not be published. Required fields are marked *