Skip to main content

Overview

Angular provides a robust testing infrastructure built on Jasmine and Karma (or Jest). The testing utilities make it easy to write unit tests, integration tests, and end-to-end tests for your applications.

Testing Setup

Basic Test Structure

Angular tests follow a standard pattern using Jasmine’s describe and it blocks.
import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent]
    }).compileComponents();
  });

  it('should create', () => {
    const fixture = TestBed.createComponent(MyComponent);
    const component = fixture.componentInstance;
    expect(component).toBeTruthy();
  });
});

TestBed

TestBed is Angular’s primary testing utility that creates a testing module for your components.

Configuring TestBed

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UserService } from './user.service';
import { UserComponent } from './user.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        UserComponent,
        HttpClientTestingModule,
        BrowserAnimationsModule
      ],
      providers: [
        UserService
      ]
    }).compileComponents();

    service = TestBed.inject(UserService);
  });

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

Standalone Components

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { StandaloneComponent } from './standalone.component';

describe('StandaloneComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [StandaloneComponent],
      providers: [
        provideHttpClient(),
        provideAnimations()
      ]
    }).compileComponents();
  });

  it('should work', () => {
    const fixture = TestBed.createComponent(StandaloneComponent);
    expect(fixture.componentInstance).toBeTruthy();
  });
});

ComponentFixture

ComponentFixture provides access to the component instance and its DOM.

Basic Usage

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

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

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

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    compiled = fixture.nativeElement;
  });

  it('should display counter value', async () => {
    component.count.set(5);
    await fixture.whenStable();
    
    expect(compiled.textContent).toContain('Count: 5');
  });

  it('should increment counter', async () => {
    component.increment();
    await fixture.whenStable();
    
    expect(component.count()).toBe(1);
  });
});

Act, Wait, Assert Pattern

For zoneless and modern Angular applications, use the Act-Wait-Assert pattern.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { AsyncComponent } from './async.component';

describe('AsyncComponent (Zoneless)', () => {
  let fixture: ComponentFixture<AsyncComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AsyncComponent],
      providers: [
        provideZonelessChangeDetection()
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(AsyncComponent);
  });

  it('should load data', async () => {
    // Act: Trigger the action
    fixture.componentInstance.loadData();
    
    // Wait: Wait for async operations to complete
    await fixture.whenStable();
    
    // Assert: Verify the result
    expect(fixture.componentInstance.data()).toBeTruthy();
    expect(fixture.nativeElement.textContent).toContain('Loaded');
  });
});
In zoneless applications, always use await fixture.whenStable() instead of fixture.detectChanges().

Testing Services

Simple Service

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

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

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

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

  it('should subtract numbers', () => {
    expect(service.subtract(5, 3)).toBe(2);
  });

  it('should multiply numbers', () => {
    expect(service.multiply(4, 3)).toBe(12);
  });
});

Service with Dependencies

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

interface User {
  id: number;
  name: string;
}

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('should fetch users', () => {
    const mockUsers: User[] = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];

    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });

  it('should handle errors', () => {
    service.getUsers().subscribe({
      next: () => fail('should have failed'),
      error: (error) => {
        expect(error.status).toBe(404);
      }
    });

    const req = httpMock.expectOne('/api/users');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });
});

Testing Components with Signals

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { TodoComponent } from './todo.component';

describe('TodoComponent with Signals', () => {
  let component: TodoComponent;
  let fixture: ComponentFixture<TodoComponent>;

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

    fixture = TestBed.createComponent(TodoComponent);
    component = fixture.componentInstance;
  });

  it('should add todo', async () => {
    const initialCount = component.todos().length;
    
    component.addTodo('New task');
    await fixture.whenStable();
    
    expect(component.todos().length).toBe(initialCount + 1);
    expect(component.todos()[initialCount].title).toBe('New task');
  });

  it('should toggle todo', async () => {
    component.addTodo('Task 1');
    await fixture.whenStable();
    
    const todoId = component.todos()[0].id;
    component.toggleTodo(todoId);
    await fixture.whenStable();
    
    expect(component.todos()[0].completed).toBe(true);
  });

  it('should compute stats correctly', async () => {
    component.addTodo('Task 1');
    component.addTodo('Task 2');
    await fixture.whenStable();
    
    expect(component.stats().total).toBe(2);
    expect(component.stats().active).toBe(2);
    expect(component.stats().completed).toBe(0);
    
    component.toggleTodo(component.todos()[0].id);
    await fixture.whenStable();
    
    expect(component.stats().active).toBe(1);
    expect(component.stats().completed).toBe(1);
  });
});

Mocking Dependencies

Mock Services

import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { DataService } from './data.service';
import { DataComponent } from './data.component';

describe('DataComponent with Mock', () => {
  let mockDataService: jasmine.SpyObj<DataService>;

  beforeEach(async () => {
    mockDataService = jasmine.createSpyObj('DataService', ['getData']);
    mockDataService.getData.and.returnValue(of(['item1', 'item2']));

    await TestBed.configureTestingModule({
      imports: [DataComponent],
      providers: [
        { provide: DataService, useValue: mockDataService }
      ]
    }).compileComponents();
  });

  it('should load data from service', async () => {
    const fixture = TestBed.createComponent(DataComponent);
    await fixture.whenStable();
    
    expect(mockDataService.getData).toHaveBeenCalled();
    expect(fixture.componentInstance.items.length).toBe(2);
  });
});

Mock Providers

import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { ProtectedComponent } from './protected.component';

class MockAuthService {
  isAuthenticated = true;
  currentUser = { id: 1, name: 'Test User' };
  
  login(credentials: any) {
    return Promise.resolve(this.currentUser);
  }
  
  logout() {
    this.isAuthenticated = false;
  }
}

describe('ProtectedComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProtectedComponent],
      providers: [
        { provide: AuthService, useClass: MockAuthService }
      ]
    }).compileComponents();
  });

  it('should show content when authenticated', async () => {
    const fixture = TestBed.createComponent(ProtectedComponent);
    await fixture.whenStable();
    
    expect(fixture.nativeElement.textContent).toContain('Protected Content');
  });
});

Testing User Interactions

Click Events

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ButtonComponent } from './button.component';

describe('ButtonComponent', () => {
  let fixture: ComponentFixture<ButtonComponent>;
  let button: HTMLButtonElement;

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

    fixture = TestBed.createComponent(ButtonComponent);
    button = fixture.nativeElement.querySelector('button');
  });

  it('should handle click', async () => {
    const component = fixture.componentInstance;
    spyOn(component, 'onClick');
    
    button.click();
    await fixture.whenStable();
    
    expect(component.onClick).toHaveBeenCalled();
  });

  it('should be disabled when loading', async () => {
    fixture.componentInstance.loading.set(true);
    await fixture.whenStable();
    
    expect(button.disabled).toBe(true);
  });
});

Form Input

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { InputComponent } from './input.component';

describe('InputComponent', () => {
  let fixture: ComponentFixture<InputComponent>;
  let input: HTMLInputElement;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [InputComponent, FormsModule]
    }).compileComponents();

    fixture = TestBed.createComponent(InputComponent);
    input = fixture.nativeElement.querySelector('input');
  });

  it('should update on input', async () => {
    input.value = 'test value';
    input.dispatchEvent(new Event('input'));
    await fixture.whenStable();
    
    expect(fixture.componentInstance.value()).toBe('test value');
  });
});

Async Testing

Testing Observables

import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { of, delay } from 'rxjs';
import { AsyncService } from './async.service';

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

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

  it('should return data after delay', fakeAsync(() => {
    let result: string | undefined;
    
    service.getDataWithDelay().subscribe(data => {
      result = data;
    });
    
    expect(result).toBeUndefined();
    
    tick(1000); // Fast-forward time
    
    expect(result).toBe('delayed data');
  }));
});

Testing Promises

import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';

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

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

  it('should fetch data', async () => {
    const data = await service.fetchData();
    expect(data).toBeTruthy();
  });

  it('should handle errors', async () => {
    try {
      await service.fetchInvalidData();
      fail('should have thrown error');
    } catch (error) {
      expect(error).toBeTruthy();
    }
  });
});

Testing Pipes

import { TitleCasePipe } from './title-case.pipe';

describe('TitleCasePipe', () => {
  let pipe: TitleCasePipe;

  beforeEach(() => {
    pipe = new TitleCasePipe();
  });

  it('should transform text to title case', () => {
    expect(pipe.transform('hello world')).toBe('Hello World');
  });

  it('should handle empty string', () => {
    expect(pipe.transform('')).toBe('');
  });

  it('should handle single word', () => {
    expect(pipe.transform('hello')).toBe('Hello');
  });
});

Testing Directives

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HighlightDirective } from './highlight.directive';

@Component({
  template: `<p appHighlight>Test</p>`,
  standalone: true,
  imports: [HighlightDirective]
})
class TestComponent {}

describe('HighlightDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let element: HTMLElement;

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

    fixture = TestBed.createComponent(TestComponent);
    element = fixture.nativeElement.querySelector('p');
  });

  it('should highlight element', async () => {
    await fixture.whenStable();
    expect(element.style.backgroundColor).toBe('yellow');
  });
});

Best Practices

Use Act-Wait-Assert

Follow the Act-Wait-Assert pattern for reliable async testing.

Mock Dependencies

Mock external dependencies to isolate units under test.

Test Behavior, Not Implementation

Focus on what components do, not how they do it.

Keep Tests Fast

Use fakeAsync and tick() to avoid real time delays.
Avoid testing private methods directly. Test public API and observable behavior instead.

Code Coverage

Run tests with coverage reporting:
ng test --code-coverage
Configure coverage thresholds in karma.conf.js:
coverageReporter: {
  type: 'html',
  dir: require('path').join(__dirname, './coverage'),
  check: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80
    }
  }
}

Additional Resources

Build docs developers (and LLMs) love