Skip to main content

Angular Testing Basics

Angular comes with a built-in testing environment, providing functionality equivalent to React + Jest/Testing Library setup out of the box.

1. Testing Environment Overview​

A project generated with ng new already has a test runner, a test framework, and testing utilities (TestBed) configured. Simply run ng test to execute your tests.

RoleStandard setup (legacy)Recent options
Test frameworkJasmineJest / Vitest
Test runnerKarma (runs in a real browser)Jest (jsdom) / Vitest (jsdom, happy-dom, browser mode)
Angular utilitiesTestBed, ComponentFixtureSame (shared)
E2E(Protractor has been retired)Playwright / Cypress

Karma Deprecation and the Move to Jest / Vitest​

Karma was deprecated in 2023 (around Angular 15), and the Angular team is exploring a new standard. The current situation is as follows.

  • Depending on the version, new projects may still default to Jasmine + Karma. Check what ng new generates for the Angular version you use.
  • The @angular-devkit/build-angular:jest builder is offered as experimental support.
  • The @angular/build:unit-test builder for Vitest is also experimental, and is a strong candidate for the new standard.
  • Existing Jest setups (such as jest-preset-angular) are a community staple and remain widely used.

TestBed and ComponentFixture have the same API regardless of whether you use Jasmine, Jest, or Vitest, so most test code does not change when you migrate. What mainly changes is how you write spies and mocks: spyOn (Jasmine) versus jest.fn() / vi.fn().

# Run all tests in watch mode
ng test

# For CI: run once (headless Chrome with Karma)
ng test --no-watch --browsers=ChromeHeadless

# With coverage
ng test --no-watch --code-coverage

Comparison with React​

AspectAngularReact / Next.js
Initial setupPreconfigured by the CLIInstall Jest / Vitest + Testing Library yourself (Next.js has an official guide)
Swapping dependenciesproviders in TestBedjest.mock or provider wrappers
Rendering a componentTestBed.createComponentrender()
Querying elementsfixture.debugElement.query / nativeElement.querySelectorscreen.getByRole, etc.
Change detectionCall fixture.detectChanges() explicitlyrender and act wait for re-rendering automatically

2. Unit Testing (Services)​

To test a service, create a test DI environment with TestBed.configureTestingModule and retrieve the service with TestBed.inject.

// counter.service.ts
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CounterService {
readonly count = signal(0);

increment(): void {
this.count.update((n) => n + 1);
}
}
// counter.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CounterService } from './counter.service';

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

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

it('increments count by 1 on increment', () => {
service.increment();
expect(service.count()).toBe(1);
});
});

TestBed is reset for every test, so state does not leak between tests even for providedIn: 'root' services. For a simple service with no dependencies, you can also instantiate it directly with new CounterService().

Mocking DI (useValue / useClass)​

Dependencies are swapped through providers. This is the biggest advantage of Angular testing.

// user-store.service.ts
import { Injectable, inject } from '@angular/core';
import { UserApiService } from './user-api.service';

@Injectable({ providedIn: 'root' })
export class UserStoreService {
private readonly api = inject(UserApiService);

async loadNames(): Promise<string[]> {
const users = await this.api.fetchUsers();
return users.map((u) => u.name);
}
}
// user-store.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { UserStoreService } from './user-store.service';
import { UserApiService } from './user-api.service';

describe('UserStoreService', () => {
it('can inject a stub with useValue', async () => {
const apiStub = {
fetchUsers: jasmine
.createSpy('fetchUsers')
.and.resolveTo([{ id: 1, name: 'Alice' }]),
};

TestBed.configureTestingModule({
providers: [{ provide: UserApiService, useValue: apiStub }],
});

const store = TestBed.inject(UserStoreService);
expect(await store.loadNames()).toEqual(['Alice']);
expect(apiStub.fetchUsers).toHaveBeenCalledTimes(1);
});
});

With Jest / Vitest, replace jasmine.createSpy(...).and.resolveTo(x) with jest.fn().mockResolvedValue(x) (or vi.fn().mockResolvedValue(x) for Vitest).

OptionUse case
useValuePass a hand-written stub object or spy
useClassReplace with a fake implementation class for tests
useFactoryBuild the mock depending on the situation
class FakeUserApiService implements Pick<UserApiService, 'fetchUsers'> {
async fetchUsers() {
return [{ id: 1, name: 'Fake' }];
}
}

TestBed.configureTestingModule({
providers: [{ provide: UserApiService, useClass: FakeUserApiService }],
});

In React you replace things per module with jest.mock('./api'), whereas in Angular you replace per DI token, so you do not need to worry about import order or hoisting.

3. Component Testing​

In a component test, you create a ComponentFixture with TestBed.createComponent and verify both the rendered DOM and the component instance. Standalone components are specified directly in imports.

// greeting.component.ts
import { Component, input, output } from '@angular/core';

@Component({
selector: 'app-greeting',
standalone: true,
template: `
<h2 data-testid="title">Hello, {{ name() }}</h2>
<button type="button" (click)="clicked.emit(name())">Greet</button>
`,
})
export class GreetingComponent {
readonly name = input.required<string>();
readonly clicked = output<string>();
}
// greeting.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';

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

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

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

it('displays the given name', () => {
fixture.componentRef.setInput('name', 'Alice');
fixture.detectChanges();

const title: HTMLElement = fixture.nativeElement.querySelector('[data-testid="title"]');
expect(title.textContent).toContain('Alice');
});
});

Main ComponentFixture APIs​

APIDescription
fixture.componentInstanceThe component instance
fixture.nativeElementThe root DOM element
fixture.debugElementAngular's debugging wrapper (query / queryAll / triggerEventHandler)
fixture.componentRef.setInput(name, value)Sets an input (@Input / input())
fixture.detectChanges()Runs change detection once and updates the DOM
fixture.whenStable()Waits for asynchronous work to finish

The key point is that the DOM is not updated until you call fixture.detectChanges() in the test (the default manual mode). After changing an input, call detectChanges() before asserting on the DOM. You can automate this with ComponentFixtureAutoDetect or fixture.autoDetectChanges().

Testing @Input / @Output​

Verify inputs with setInput, and outputs with subscribe (the signal-based output() can also be subscribed to via OutputEmitterRef.subscribe) or with spies. Trigger DOM interactions with click() or dispatchEvent.

it('emits name via clicked when the button is clicked', () => {
fixture.componentRef.setInput('name', 'Bob');
fixture.detectChanges();

const emitted: string[] = [];
fixture.componentInstance.clicked.subscribe((v) => emitted.push(v));

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

expect(emitted).toEqual(['Bob']);
});

For traditional components using the @Input() decorator, assigning directly with fixture.componentInstance.name = 'Bob' also works, but updates may not be reflected in OnPush components, so using setInput is safer.

Testing the New Control Flow and Signals​

Built-in control flow such as @if / @for can be tested as usual by asserting on the rendered DOM. For signals, change the value directly and call detectChanges() to re-render.

@Component({
standalone: true,
template: `
@for (item of items(); track item) {
<li>{{ item }}</li>
} @empty {
<li class="empty">No data</li>
}
`,
})
class ListComponent {
items = signal<string[]>([]);
}

it('shows @empty when the list is empty', () => {
const fixture = TestBed.createComponent(ListComponent);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.empty')).not.toBeNull();

fixture.componentInstance.items.set(['a', 'b']);
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('li').length).toBe(2);
});

Mocking Child Components​

If you do not want to depend on the details of a child component, swap it for a stub component. Use overrideComponent to replace imports.

@Component({ selector: 'app-user-card', standalone: true, template: '' })
class UserCardStubComponent {
readonly user = input<unknown>();
}

TestBed.configureTestingModule({ imports: [UserListComponent] });
TestBed.overrideComponent(UserListComponent, {
remove: { imports: [UserCardComponent] },
add: { imports: [UserCardStubComponent] },
});

That said, Angular testing often favors integration-leaning tests that render child components too. Limiting mocks to heavy child components or those with external dependencies makes tests less brittle.

Comparison with React Testing Library​

OperationAngular (TestBed)React Testing Library
RenderTestBed.createComponent(X)render(<X />)
props / inputsfixture.componentRef.setInputrender(<X name="a" />) / rerender
Query elementsfixture.nativeElement.querySelector / debugElement.query(By.css(...))screen.getByRole / getByText
Eventsbutton.click() / triggerEventHandlerfireEvent.click / userEvent.click
Re-renderfixture.detectChanges()Automatic (handled within act)
Async waitingawait fixture.whenStable()await screen.findBy... / waitFor

In Angular, installing @testing-library/angular also lets you write in a style close to render / screen / fireEvent, making user-centric tests easier to write.

4. Testing HTTP Requests​

Use provideHttpClientTesting to verify request contents and inject responses without making real network calls. The former HttpClientTestingModule is deprecated; use the functional providers today.

// user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly http = inject(HttpClient);

getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}

createUser(body: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>('/api/users', body);
}
}
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing';

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

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

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

it('calls GET /api/users/1 and returns the result', () => {
let result: User | undefined;
service.getUser(1).subscribe((u) => (result = u));

const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush({ id: 1, name: 'Alice', email: '[email protected]' });

expect(result?.name).toBe('Alice');
});

it('verifies the POST body', () => {
service.createUser({ name: 'Bob', email: '[email protected]' }).subscribe();

const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({ name: 'Bob', email: '[email protected]' });
req.flush({ id: 2, name: 'Bob', email: '[email protected]' });
});

it('handles HTTP errors', () => {
let status: number | undefined;
service.getUser(99).subscribe({ error: (e) => (status = e.status) });

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

expect(status).toBe(404);
});
});

Specify provideHttpClient() and provideHttpClientTesting() in this order. To verify interceptors as well, specify provideHttpClient(withInterceptors([...])).

HttpTestingController methodDescription
expectOne(url or predicate)Asserts that exactly one matching request exists and returns it
match(predicate)Returns all matching requests
expectNone(predicate)Asserts that no matching request exists
verify()Asserts that there are no outstanding requests

req.flush(data) returns a response, and req.error(new ProgressEvent('error')) simulates a network error. In React, mocking the network layer with MSW (Mock Service Worker) is the mainstream approach, while in Angular HttpTestingController is provided as the standard mechanism.

5. E2E Testing​

While unit and component tests verify components on top of TestBed, E2E tests drive and verify the whole application in a real browser.

  • Protractor is deprecated (support ended in 2023) and is not included in new projects. Existing projects need to migrate to another tool.
  • The main options today are Playwright and Cypress. Neither depends on Angular-specific features, and you can use them just as you would for any web application.
  • If the project has no E2E builder installed, ng e2e walks you through choosing and installing a tool.
AspectPlaywrightCypress
BrowsersChromium / Firefox / WebKitChromium-based / Firefox / WebKit (experimental)
Parallel executionStrong out of the boxPaid cloud or separate setup
LanguageTypeScript / JavaScript, etc.TypeScript / JavaScript
Auto-waitingYesYes
// e2e/users.spec.ts (Playwright)
import { test, expect } from '@playwright/test';

test('shows the user list', async ({ page }) => {
await page.goto('/users');
await expect(page.getByRole('heading', { name: 'User List' })).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(3);
});

React / Next.js uses the same Playwright / Cypress, so your E2E knowledge carries over as is.

6. Testing Best Practices​

Design Code to Be Testable​

  • Receive dependencies via inject() or the constructor: With DI, you can mock simply by swapping providers. Avoid implementations that create their own instances, such as new HttpClient().
  • Put logic in services or pure functions: Keeping components thin and placing business logic in services lets you test quickly without TestBed.
  • Test functional guards and resolvers with TestBed.runInInjectionContext: Functions that use inject() require an injection context.
it('redirects to /login when not logged in', () => {
TestBed.configureTestingModule({
providers: [{ provide: AuthService, useValue: { isLoggedIn: () => false } }],
});

const result = TestBed.runInInjectionContext(() =>
authGuard({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot),
);

expect(result.toString()).toBe('/login');
});
  • Do not tie selectors to implementation details: Using data-testid, roles (role), or labels is less brittle than CSS classes.
  • For async, use fakeAsync / tick or await fixture.whenStable(): Avoid tests that depend on real wait times (setTimeout).

Snapshot Testing​

With Jest, DOM snapshots via toMatchSnapshot are possible, but they are not widely recommended in Angular. The generated DOM contains many attributes and comment nodes, so diffs tend to become noise. If you use them, limit them to small, stable UI parts or serialized output (such as configuration objects), and verify behavior with ordinary assertions.

Setting Coverage Goals​

ng test --code-coverage outputs a coverage report to coverage/.

  • If you set a numeric target, aim for roughly 70-80% overall, and do not make 100% the goal.
  • Thresholds can be set with coverageReporter.check in karma.conf.js (Karma) or coverageThreshold in Jest, so CI can fail when coverage drops below them.
  • Business logic (services, guards, pipes) should be high; template-only components can be lower.
  • Use coverage to find untested areas, not as a measure of quality itself.

Test Structure Guidelines​

TypeTargetSpeedProportion
UnitServices, pipes, pure functions, guardsFastMany
ComponentA single component + its templateMediumMedium
E2EKey user flowsSlowFew