Docs
/
Angular
Chapter 16

16 — Testing

Testing Pyramid in Angular

        /  E2E Tests  \        ← Few (Cypress/Playwright)
       / Integration   \       ← Some (Component + Service tests)
      /   Unit Tests    \      ← Many (Pure logic, pipes, services)

Unit Testing Setup

Angular CLI ships with Jasmine + Karma by default. Many teams switch to Jest.

Switch to Jest

npm install --save-dev jest @angular-builders/jest @types/jest
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
// angular.json
"test": {
  "builder": "@angular-builders/jest:run",
  "options": { "tsConfig": "tsconfig.spec.json" }
}
ng test              # Run tests
ng test --watch      # Watch mode
ng test --coverage   # Coverage report

Testing Services

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserService,
        provideHttpClient(),
        provideHttpClientTesting(),
      ],
    });

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

  afterEach(() => {
    httpMock.verify();   // Ensure no unmatched requests
  });

  it('should fetch users', () => {
    const mockUsers: User[] = [
      { id: '1', name: 'Alice', email: 'alice@test.com' },
    ];

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

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

  it('should handle errors', () => {
    service.getAll().subscribe({
      error: (err) => expect(err.status).toBe(500),
    });

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

Testing Components

Basic Component Test

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

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

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

  it('should display user name', () => {
    component.user = { id: '1', name: 'Alice', email: 'alice@test.com' };
    fixture.detectChanges();

    const el: HTMLElement = fixture.nativeElement;
    expect(el.querySelector('h3')?.textContent).toContain('Alice');
  });

  it('should emit selected event on click', () => {
    const user = { id: '1', name: 'Alice', email: 'alice@test.com' };
    component.user = user;
    fixture.detectChanges();

    let emitted: User | undefined;
    component.selected.subscribe(u => emitted = u);

    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(emitted).toEqual(user);
  });
});

Component with Dependencies

describe('DashboardComponent', () => {
  let component: DashboardComponent;
  let fixture: ComponentFixture<DashboardComponent>;
  let mockUserService: jasmine.SpyObj<UserService>;

  beforeEach(async () => {
    mockUserService = jasmine.createSpyObj('UserService', ['getAll']);
    mockUserService.getAll.and.returnValue(of([{ id: '1', name: 'Alice' }]));

    await TestBed.configureTestingModule({
      imports: [DashboardComponent],
      providers: [
        { provide: UserService, useValue: mockUserService },
      ],
    }).compileComponents();

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

  it('should load users on init', () => {
    expect(mockUserService.getAll).toHaveBeenCalled();
    expect(component.users.length).toBe(1);
  });
});

Testing Reactive Forms

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

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

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

  it('should be invalid when empty', () => {
    expect(component.form.valid).toBeFalse();
  });

  it('should be valid with correct data', () => {
    component.form.patchValue({
      email: 'alice@test.com',
      password: 'password123',
    });
    expect(component.form.valid).toBeTrue();
  });

  it('should show email error when invalid', () => {
    const email = component.form.controls['email'];
    email.setValue('invalid');
    email.markAsTouched();
    fixture.detectChanges();

    const errorEl = fixture.nativeElement.querySelector('.error');
    expect(errorEl).toBeTruthy();
  });
});

Testing Signals

describe('CounterComponent', () => {
  it('should increment count', () => {
    const fixture = TestBed.createComponent(CounterComponent);
    const component = fixture.componentInstance;

    expect(component.count()).toBe(0);

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

Testing Pipes

describe('TruncatePipe', () => {
  const pipe = new TruncatePipe();

  it('should truncate long strings', () => {
    expect(pipe.transform('Hello World', 5)).toBe('Hello...');
  });

  it('should not truncate short strings', () => {
    expect(pipe.transform('Hi', 5)).toBe('Hi');
  });

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

Testing Guards

describe('authGuard', () => {
  it('should allow authenticated users', () => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AuthService, useValue: { isAuthenticated: () => true } },
        provideRouter([]),
      ],
    });

    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard' } as any),
    );

    expect(result).toBeTrue();
  });

  it('should redirect unauthenticated users', () => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AuthService, useValue: { isAuthenticated: () => false } },
        provideRouter([]),
      ],
    });

    const result = TestBed.runInInjectionContext(() =>
      authGuard({} as any, { url: '/dashboard' } as any),
    );

    expect(result).toBeInstanceOf(UrlTree);
  });
});

E2E Testing with Playwright

ng add @angular/playwright
# or
npm install --save-dev @playwright/test
npx playwright install
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('should display login form', async ({ page }) => {
    await expect(page.locator('h1')).toHaveText('Login');
    await expect(page.locator('input[name="email"]')).toBeVisible();
  });

  test('should login with valid credentials', async ({ page }) => {
    await page.fill('input[name="email"]', 'alice@test.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('.welcome')).toContainText('Alice');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('input[name="email"]', 'wrong@test.com');
    await page.fill('input[name="password"]', 'wrong');
    await page.click('button[type="submit"]');

    await expect(page.locator('.error')).toBeVisible();
  });
});

Key Takeaways

  • Write many unit tests (services, pipes, pure logic), some component tests, few E2E tests
  • Use HttpTestingController to mock HTTP — never call real APIs in unit tests
  • Use jasmine.createSpyObj or Jest mocks to mock service dependencies
  • fixture.detectChanges() triggers change detection — call it after setting inputs
  • Test signals directly by calling signal() — no async complexity
  • TestBed.runInInjectionContext() is needed for testing functional guards/interceptors
  • Prefer Playwright over Protractor (deprecated) for E2E tests