メインコンテンツまでスキップ

Angularテストの基礎

Angularにはテスト環境が標準で組み込まれており、React + Jest/Testing Library のセットアップに相当する機能が最初から利用可能です。

1. テスト環境の概要​

ng new で生成したプロジェクトには、テストランナー・テストフレームワーク・テスト用ユーティリティ(TestBed)があらかじめ構成されています。ng test を実行するだけでテストが走ります。

役割標準構成(従来)最近の選択肢
テストフレームワークJasmineJest / Vitest
テストランナーKarma(実ブラウザで実行)Jest(jsdom)/ Vitest(jsdom, happy-dom, browser mode)
Angular 用ユーティリティTestBed, ComponentFixture同左(共通)
E2E(Protractor は廃止済み)Playwright / Cypress

Karma 非推奨と Jest / Vitest への移行​

Karma は 2023 年に非推奨となり(Angular 15 前後)、Angular チームは新しい標準を模索しています。現状の整理は次のとおりです。

  • 新規プロジェクトの既定は、バージョンによっては引き続き Jasmine + Karma です。使用中の Angular のバージョンで ng new が何を生成するかを確認してください。
  • @angular-devkit/build-angular:jest ビルダーは実験的サポートとして提供されています。
  • Vitest 向けの @angular/build:unit-test ビルダーも実験的サポートとして提供されており、新しい標準の有力候補です。
  • 既存の Jest 構成(jest-preset-angular など)はコミュニティ製の定番で、今も広く使われています。

TestBed や ComponentFixture は Jasmine / Jest / Vitest のどれでも同じ API で使えるため、テストコードの大半は移行しても変わりません。変わるのは主に spyOn(Jasmine)と jest.fn() / vi.fn() といったスパイ・モックの書き方です。

# 全テストを監視モードで実行
ng test

# CI 向け: 1 回だけ実行(Karma の場合はヘッドレス Chrome)
ng test --no-watch --browsers=ChromeHeadless

# カバレッジ付き
ng test --no-watch --code-coverage

React との比較​

観点AngularReact / Next.js
初期セットアップCLI が構成済みJest / Vitest + Testing Library を自分で導入(Next.js は公式ガイドあり)
DI の差し替えTestBed の providersjest.mock やプロバイダーのラッパー
コンポーネント描画TestBed.createComponentrender()
要素の取得fixture.debugElement.query / nativeElement.querySelectorscreen.getByRole など
変更検知fixture.detectChanges() を明示的に呼ぶrender や act が自動で再描画を待つ

2. ユニットテスト(サービス)​

サービスのテストでは、TestBed.configureTestingModule でテスト用の DI 環境を作り、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('increment で count が 1 増える', () => {
service.increment();
expect(service.count()).toBe(1);
});
});

TestBed はテストごとにリセットされるため、providedIn: 'root' のサービスでもテスト間で状態が漏れません。依存のない単純なサービスなら、new CounterService() で直接生成するテストも可能です。

DI のモック(useValue / useClass)​

依存サービスは providers で差し替えます。これが Angular のテストにおける最大の利点です。

// 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('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);
});
});

Jest / Vitest の場合は jasmine.createSpy(...).and.resolveTo(x) を jest.fn().mockResolvedValue(x)(Vitest なら vi.fn().mockResolvedValue(x))に置き換えます。

指定方法用途
useValue手書きのスタブオブジェクトやスパイを渡す
useClassテスト用のフェイク実装クラスに差し替える
useFactory状況に応じてモックを組み立てる
class FakeUserApiService implements Pick<UserApiService, 'fetchUsers'> {
async fetchUsers() {
return [{ id: 1, name: 'Fake' }];
}
}

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

React では jest.mock('./api') でモジュール単位に差し替えますが、Angular では DI トークン単位で差し替えるため、モジュールのインポート順序や巻き上げ(hoisting)を気にする必要がありません。

3. コンポーネントテスト​

コンポーネントのテストでは TestBed.createComponent で ComponentFixture を作り、描画された DOM とコンポーネントのインスタンスの両方を検証します。スタンドアロンコンポーネントは imports に直接指定します。

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

@Component({
selector: 'app-greeting',
standalone: true,
template: `
<h2 data-testid="title">こんにちは、{{ name() }} さん</h2>
<button type="button" (click)="clicked.emit(name())">挨拶する</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('入力された名前を表示する', () => {
fixture.componentRef.setInput('name', 'Alice');
fixture.detectChanges();

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

ComponentFixture の主な API​

API説明
fixture.componentInstanceコンポーネントのインスタンス
fixture.nativeElementルートの DOM 要素
fixture.debugElementAngular のデバッグ用ラッパー(query / queryAll / triggerEventHandler)
fixture.componentRef.setInput(name, value)入力(@Input / input())を設定する
fixture.detectChanges()変更検知を 1 回実行して DOM を更新する
fixture.whenStable()非同期処理の完了を待つ

重要なのは、テスト内で fixture.detectChanges() を呼ぶまで DOM は更新されない点です(既定の手動モード)。入力を変えたら detectChanges() を呼んでから DOM を検証してください。ComponentFixtureAutoDetect や fixture.autoDetectChanges() で自動化することも可能です。

@Input / @Output のテスト​

入力は setInput、出力は subscribe(シグナルベースの output() も OutputEmitterRef.subscribe で購読できます)またはスパイで検証します。DOM 操作は click() や dispatchEvent で行います。

it('ボタンクリックで clicked が name を発行する', () => {
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']);
});

@Input() デコレータを使う従来のコンポーネントでは、fixture.componentInstance.name = 'Bob' と直接代入しても動きますが、OnPush コンポーネントでは更新が反映されないことがあるため、setInput を使うのが安全です。

新しい制御フローとシグナルのテスト​

@if / @for などの組み込み制御フローは、描画結果の DOM を検証すれば通常どおりテストできます。シグナルは値を直接変更し、detectChanges() を呼んで再描画します。

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

it('空のとき @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);
});

子コンポーネントのモック​

子コンポーネントの詳細に依存したくない場合は、スタブコンポーネントに差し替えます。overrideComponent で 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] },
});

一方で、Angular のテストでは子コンポーネントも含めて描画する「統合寄り」のテストが推奨されることも多く、モックは重い子コンポーネントや外部依存がある場合に限定するとテストが壊れにくくなります。

React Testing Library との比較​

操作Angular(TestBed)React Testing Library
描画TestBed.createComponent(X)render(<X />)
props / 入力fixture.componentRef.setInputrender(<X name="a" />) / rerender
要素取得fixture.nativeElement.querySelector / debugElement.query(By.css(...))screen.getByRole / getByText
イベントbutton.click() / triggerEventHandlerfireEvent.click / userEvent.click
再描画fixture.detectChanges()自動(act 内で処理)
非同期待機await fixture.whenStable()await screen.findBy... / waitFor

Angular でも @testing-library/angular を導入すると、render / screen / fireEvent に近い書き方ができ、ユーザー視点のテストを書きやすくなります。

4. HTTPリクエストのテスト​

実際の通信を行わず、リクエスト内容の検証とレスポンスの注入を行うために provideHttpClientTesting を使います。以前の HttpClientTestingModule は非推奨で、現在は関数型プロバイダーを使います。

// 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(); // 未処理のリクエストが残っていないことを確認
});

it('GET /api/users/1 を呼び出して結果を返す', () => {
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('POST のボディを検証する', () => {
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('HTTP エラーを扱える', () => {
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);
});
});

provideHttpClient() と provideHttpClientTesting() はこの順序で指定します。インターセプターを含めて検証したい場合は provideHttpClient(withInterceptors([...])) を指定します。

HttpTestingController のメソッド説明
expectOne(urlまたは条件)条件に合うリクエストがちょうど 1 件あることを検証して取得
match(条件)条件に合うリクエストをすべて取得
expectNone(条件)該当リクエストがないことを検証
verify()未処理のリクエストがないことを検証

req.flush(data) でレスポンスを返し、req.error(new ProgressEvent('error')) でネットワークエラーを再現できます。React では MSW(Mock Service Worker)でネットワーク層をモックするのが主流ですが、Angular では HttpTestingController が標準の仕組みとして用意されています。

5. E2Eテスト​

ユニット / コンポーネントテストが TestBed 上でコンポーネントを検証するのに対し、E2E テストは実ブラウザでアプリ全体を操作して検証します。

  • **Protractor は非推奨(2023 年にサポート終了)**で、新規プロジェクトには含まれません。既存プロジェクトは別のツールへ移行が必要です。
  • 現在の主な選択肢は Playwright と Cypress です。どちらも Angular 固有の機能に依存せず、通常の Web アプリと同じように扱えます。
  • ng e2e は、プロジェクトに E2E ビルダーが未導入の場合、使用するツールを選んで導入するフローになります。
観点PlaywrightCypress
ブラウザChromium / Firefox / WebKitChromium 系 / Firefox / WebKit(実験的)
並列実行標準で強力有料のクラウドまたは別途構成
言語TypeScript / JavaScript ほかTypeScript / JavaScript
自動待機ありあり
// e2e/users.spec.ts (Playwright)
import { test, expect } from '@playwright/test';

test('ユーザー一覧が表示される', async ({ page }) => {
await page.goto('/users');
await expect(page.getByRole('heading', { name: 'ユーザー一覧' })).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(3);
});

React / Next.js でも同じ Playwright / Cypress が使われるため、E2E の知識はそのまま流用できます。

6. テストのベストプラクティス​

テストしやすいコード設計​

  • 依存は inject() またはコンストラクターで受け取る: DI を使っていれば providers で差し替えるだけでモックできます。new HttpClient() のように自分で生成する実装は避けます。
  • ロジックはサービスや純粋関数へ: コンポーネントを薄く保ち、ビジネスロジックはサービスに置くと、TestBed なしで高速にテストできます。
  • 関数型ガード・リゾルバーは TestBed.runInInjectionContext でテスト: inject() を使う関数は注入コンテキストが必要です。
it('未ログインなら /login へリダイレクトする', () => {
TestBed.configureTestingModule({
providers: [{ provide: AuthService, useValue: { isLoggedIn: () => false } }],
});

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

expect(result.toString()).toBe('/login');
});
  • セレクターは実装詳細に依存させない: CSS クラスよりも data-testid やロール(role)、ラベルを使うと壊れにくくなります。
  • 非同期は fakeAsync / tick または await fixture.whenStable(): 実際の待機時間(setTimeout)に依存するテストは避けます。

スナップショットテスト​

Jest を使う場合は toMatchSnapshot による DOM のスナップショットも可能ですが、Angular ではあまり推奨されません。生成される DOM に属性やコメントノードが多く含まれ、差分がノイズになりやすいためです。使うなら、小さく安定した UI 部品や、シリアライズされた出力(設定オブジェクトなど)に限定し、振る舞いは通常のアサーションで検証するのがよいでしょう。

カバレッジの目標設定​

ng test --code-coverage でカバレッジレポートが coverage/ に出力されます。

  • 数値目標を設けるなら、全体で 70〜80% 程度を目安にし、100% を目的にしないでください。
  • 閾値は karma.conf.js の coverageReporter.check(Karma)や Jest の coverageThreshold で設定でき、CI で下回ったら失敗させられます。
  • ビジネスロジック(サービス、ガード、パイプ)は高く、テンプレートだけのコンポーネントは低めでも構いません。
  • カバレッジは「テストされていない箇所を見つける」ために使い、品質そのものの指標とは考えないようにします。

テスト構成の目安​

種類対象速度比率の目安
ユニットサービス、パイプ、純粋関数、ガード速い多い
コンポーネントコンポーネント単体 + テンプレート中程度中程度
E2E主要なユーザーフロー遅い少ない