← 解説「DIと差し替えやすさ — テストで効く御利益」に戻る
サンプルコードで身につける: DIと差し替えやすさ — テストで効く御利益
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1差し替えの前提 — newせずDIで受け取る
差し替えができる書き方とできない書き方の対比です。new UserService() と自分で作ると実体がコードに焼き付き、外から変更できません。inject() で受け取っていれば、実体を決めるのはDIコンテナなので、登録を変えるだけで中身を交換できます。
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
// NG: 自分で new すると実体が固定され、テストで差し替えられない
export class BadUserList {
private users = new UserService();
}
// OK: 目印(クラス)だけ指定して、実体はDIコンテナに任せる
@Component({
selector: 'app-user-list',
template: '<p>ユーザー一覧</p>',
})
export class UserList {
private users = inject(UserService);
}
2TestBed でフェイクに差し替える最小例
テストでの差し替えの基本形です。TestBed.configureTestingModule の providers に { provide: 本物, useValue: フェイク } と登録すると、コンポーネントが inject(UserService) したときフェイクが渡されます。コンポーネント側のコードは1文字も変えていない点が重要です。
import { TestBed } from '@angular/core/testing';
import { UserList } from './user-list';
import { UserService } from './user.service';
// 決まった値を返すだけのフェイク
const fakeUserService = {
getUsers: () => ['テスト太郎', 'テスト花子'],
};
it('一覧が表示される', () => {
TestBed.configureTestingModule({
imports: [UserList],
// UserService の代わりにフェイクを配る
providers: [{ provide: UserService, useValue: fakeUserService }],
});
const fixture = TestBed.createComponent(UserList);
fixture.detectChanges();
const text = fixture.nativeElement.textContent;
expect(text).toContain('テスト太郎');
});
3通信するサービスを通信なしでテストする
HttpClient を使う本物のサービスをフェイクに差し替えると、サーバーなしでコンポーネントの表示ロジックを検証できます。テストがネットワークの状態に左右されず、速く安定して動くのが実利です。「通信部分を切り離せる設計」はDIがあってこそ成り立ちます。
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { of } from 'rxjs';
// 本物: サーバーと通信する
@Injectable({ providedIn: 'root' })
export class ProductApi {
private http = inject(HttpClient);
fetchProducts() {
return this.http.get<string[]>('/api/products');
}
}
// フェイク: 同じ形のメソッドで固定データを返す
// (of() は「すぐ値を流す Observable」を作る)
const fakeProductApi = {
fetchProducts: () => of(['ノート', 'ペン']),
};
// テスト側の providers で差し替えれば、通信は一切発生しない:
// providers: [{ provide: ProductApi, useValue: fakeProductApi }]
4失敗ケースをフェイクで再現する
差し替えの真価は「本物では起こしにくい状況を自由に作れる」ことです。通信エラーを返すフェイクを渡せば、エラーメッセージ表示のテストが確実に再現できます。本物のサーバーを落としてテストするわけにはいかないので、フェイクならではの使い方です。
import { TestBed } from '@angular/core/testing';
import { throwError } from 'rxjs';
import { ProductList } from './product-list';
import { ProductApi } from './product-api';
// 必ず通信エラーになるフェイク
const errorApi = {
fetchProducts: () => throwError(() => new Error('接続失敗')),
};
it('通信エラー時にメッセージが表示される', () => {
TestBed.configureTestingModule({
imports: [ProductList],
// エラーを起こすフェイクに差し替える
providers: [{ provide: ProductApi, useValue: errorApi }],
});
const fixture = TestBed.createComponent(ProductList);
fixture.detectChanges();
const text = fixture.nativeElement.textContent;
expect(text).toContain('読み込みに失敗しました');
});
5InjectionToken もテストで差し替えられる
差し替えられるのはクラスだけではなく、InjectionToken で配った値も同じ仕組みで交換できます。本番のAPIのURLをテスト用に差し替える例で、「DIで配ったものはすべて差し替え可能」という原則を確認できます。設定値をトークンで配る設計はテストのしやすさにも直結します。
import { TestBed } from '@angular/core/testing';
import { inject, Injectable } from '@angular/core';
import { API_BASE_URL } from './tokens';
@Injectable({ providedIn: 'root' })
export class UserApi {
private baseUrl = inject(API_BASE_URL);
usersUrl() {
return this.baseUrl + '/users';
}
}
it('ベースURLからリクエストURLを組み立てる', () => {
TestBed.configureTestingModule({
// トークンの値もテスト用に差し替えられる
providers: [{ provide: API_BASE_URL, useValue: 'http://test.local' }],
});
const api = TestBed.inject(UserApi);
expect(api.usersUrl()).toBe('http://test.local/users');
});