Skip to content

Angular Testing Explained — Unit Tests and Integration Tests

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular Testing Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular testing uses Jasmine and TestBed to verify components, services, directives, pipes, and HTTP interactions work correctly through unit and integration tests.

What You'll Learn

  • How to set up TestBed for component testing
  • How to test components with inputs, outputs, and dependencies
  • How to test services with HTTP mocking
  • How to test directives and pipes
  • Best practices for Angular testing

Why It Matters

Automated tests catch regressions early, document how components work, and give you confidence to refactor. Without tests, a change in one component can silently break another.

Real-World Use

Durga Antivirus Pro runs over 2000 tests on every pull request, covering threat detection services, dashboard components, and form validations. Tests run in under 30 seconds and catch 95% of regressions before deployment.

flowchart LR
    A[Developer] --> B[ng test]
    B --> C[Karma / Jest]
    C --> D[TestBed]
    D --> E[Component]
    D --> F[Service]
    D --> G[Pipe]
    C --> H[Jasmine Runner]
    H --> I[Results]
    style A fill:#f97316,color:#fff

Testing a Component

Set up a component with TestBed:

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { GreetingComponent } from "./greeting.component";

describe("GreetingComponent", () => {
  let component: GreetingComponent;
  let fixture: ComponentFixture<GreetingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [GreetingComponent] // standalone component imports itself
    }).compileComponents();

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

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

  it("should display the name", () => {
    component.name = "Alice";
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector("h1")?.textContent).toContain("Alice");
  });

  it("should emit greet event on button click", () => {
    spyOn(component.greet, "emit");
    const button = fixture.nativeElement.querySelector("button");
    button?.click();
    expect(component.greet.emit).toHaveBeenCalledWith("Alice");
  });
});

Expected output: Three tests pass: component creation, name display, and event emission.

TestBed.configureTestingModule sets up the testing module. createComponent creates the component instance. detectChanges triggers change detection. The component's DOM is available through fixture.nativeElement.

Testing with Mock Dependencies

Mock services and router dependencies:

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { UserProfileComponent } from "./user-profile.component";
import { UserService } from "./user.service";
import { of } from "rxjs";

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

  const mockUser = { id: 1, name: "John Doe", email: "john@example.com" };

  beforeEach(async () => {
    mockUserService = jasmine.createSpyObj("UserService", ["getUserById"]);
    mockUserService.getUserById.and.returnValue(of(mockUser));

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

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

  it("should load user data on init", () => {
    expect(component.user?.name).toBe("John Doe");
  });

  it("should display user name in template", () => {
    const nameEl = fixture.nativeElement.querySelector(".user-name");
    expect(nameEl?.textContent).toContain("John Doe");
  });
});

Expected output: The component loads and displays the mock user without making a real HTTP call.

jasmine.createSpyObj creates a mock object with spy methods. and.returnValue sets the return value. Providing the mock in providers replaces the real service in the test.

Testing a Service with HTTP

Use HttpTestingController to mock HTTP calls:

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(); // Ensure no outstanding requests
  });

  it("should fetch users", () => {
    const mockUsers = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];

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

    const req = httpMock.expectOne("https://jsonplaceholder.typicode.com/users");
    expect(req.request.method).toBe("GET");
    req.flush(mockUsers);
  });

  it("should handle HTTP errors", () => {
    service.getUsers().subscribe({
      error: (error) => {
        expect(error.status).toBe(500);
      }
    });

    const req = httpMock.expectOne("https://jsonplaceholder.typicode.com/users");
    req.flush("Server Error", { status: 500, statusText: "Internal Server Error" });
  });
});

Expected output: The service test verifies GET requests and error handling without a real server.

HttpTestingController intercepts all HTTP requests. expectOne asserts that exactly one request to the given URL was made. flush provides the mock response. verify ensures no unmatched requests remain.

Testing a Directive

Test directives with a host component:

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

@Component({
  standalone: true,
  imports: [HighlightDirective],
  template: `<p [appHighlight]="'red'">Test text</p>`
})
class TestHostComponent {}

describe("HighlightDirective", () => {
  let fixture: ComponentFixture<TestHostComponent>;
  let pElement: HTMLElement;

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

    fixture = TestBed.createComponent(TestHostComponent);
    pElement = fixture.nativeElement.querySelector("p")!;
  });

  it("should highlight on mouse enter", () => {
    pElement.dispatchEvent(new Event("mouseenter"));
    expect(pElement.style.backgroundColor).toBe("red");
  });

  it("should remove highlight on mouse leave", () => {
    pElement.dispatchEvent(new Event("mouseenter"));
    pElement.dispatchEvent(new Event("mouseleave"));
    expect(pElement.style.backgroundColor).toBe("transparent");
  });
});

Expected output: The directive properly sets and removes background color on hover events.

Testing directives requires a host component that uses the directive. You can then interact with the DOM and verify the directive's behavior through native events.

Testing a Pipe

Pipes are pure functions and easy to test:

import { TruncatePipe } from "./truncate.pipe";

describe("TruncatePipe", () => {
  let pipe: TruncatePipe;

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

  it("should return the same string if shorter than max length", () => {
    expect(pipe.transform("Hello")).toBe("Hello");
  });

  it("should truncate and add suffix", () => {
    expect(pipe.transform("Hello World Long Text", 5)).toBe("Hello...");
  });

  it("should use custom suffix", () => {
    expect(pipe.transform("Hello World", 5, "---")).toBe("Hello---");
  });

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

  it("should handle null value", () => {
    expect(pipe.transform(null as any)).toBe("");
  });
});

Expected output: All pipe tests pass, covering normal usage, edge cases, and boundary conditions.

Pipes are the simplest Angular construct to test because they are pure functions with no dependencies. Test the transform method directly with various inputs.

Common Mistakes

  1. Not calling detectChanges — Without detectChanges, the template does not update. Tests pass against outdated values.

  2. Testing implementation details — Test the rendered DOM and emitted events, not internal component state. Tests that check private methods break on Refactoring.

  3. Not cleaning up HTTP mock — Unmatched requests from one test can make another test fail. Always call httpMock.verify() in afterEach.

  4. Async tests without fakeAsync — HTTP calls and timers need fakeAsync or async helpers for proper timing control.

  5. Too much setup in beforeEach — Complex setups make tests hard to read. Extract shared setup into helper functions.

Practice Questions

  1. What is TestBed in Angular testing? A testing utility that configures and creates Angular modules for Integration Testing of components and services.

  2. How do you mock an HTTP service? Use HttpClientTestingModule and HttpTestingController to intercept and respond to HTTP requests.

  3. What does fixture.detectChanges() do? It triggers Angular's change detection, updating the template with current component state.

  4. How do you test a component's event emission? Spy on the Output property with spyOn(component.eventName, 'emit') and trigger the event.

  5. What is the purpose of async in TestBed? compileComponents() is async because it compiles external templates and styles. Use await or async.

Challenge

Write a complete test suite for a LoginFormComponent with email and password fields. Test: form renders with all fields, valid email shows no error, invalid email shows error, submit button is disabled when form is invalid, submit emits login event with correct data, and loading state disables the form button.

FAQ

Should I use Jasmine or Jest with Angular?

Jasmine is the default. Jest works with Angular but requires additional configuration via @angular-builders/jest or jest-preset-angular.

What is the difference between unit and integration tests?

Unit tests test a single class in isolation. Integration tests verify that multiple classes work together. Both use TestBed.

How do I test routing?

Use RouterTestingModule.withRoutes(routes) and inject the Router to navigate programmatically.

Can I test the template without TestBed?

For simple strings, yes. For component templates, TestBed is required to compile and render them.

How do I test file downloads?

Mock the HttpClient response and verify the blob handling logic. Test the download trigger separately.

Mini Project

Write a comprehensive test suite for a TaskManagerComponent and its dependencies. Create: unit tests for the TaskService (HTTP mock with CRUD operations), component tests for TaskListComponent (renders tasks, marks complete, deletes), integration tests for TaskFormComponent (validation, submission), and a pipe test for the TaskFilterPipe. Achieve 100% coverage for the service and pipe, 80%+ for components.

What's Next

Continue with Internationalization and NgRx:

Angular i18n, Angular State Management, Angular Forms Validation

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro