Dev Study
← 解説「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');
});