Dev Study
← 解説「HTTPを使うサービスのテスト — 本物のAPIは呼ばずに検証する」に戻る

サンプルコードで身につける: HTTPを使うサービスのテスト — 本物のAPIは呼ばずに検証する

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1テスト対象のサービスと道具立て

テスト対象は HttpClient でAPIを呼ぶ普通のサービスです。テスト側では providers に2つの関数を登録するだけで、通信先が偽物に差し替わります。サービス側のコードは一切変更しません。

// user.service.ts — テスト対象(普通のサービス。変更は不要)
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  getUsers(): Observable<string[]> {
    return this.http.get<string[]>('/api/users');
  }
}

// テスト側のセットアップ — これだけで通信が偽物に差し替わる
TestBed.configureTestingModule({
  providers: [
    provideHttpClient(),        // 先: 本物のHttpClient一式
    provideHttpClientTesting(), // 後: 通信部分だけ偽物に上書き
  ],
});

2expectOne と flush の基本の流れ

サービスのメソッドを呼ぶとリクエストが偽の通信層に溜まります。expectOne で捕まえ、flush で偽レスポンスを返すと subscribe のコールバックが動きます。呼ぶ順番がそのまま通信の一生を表しています。

it('一覧を取得できる', () => {
  const service = TestBed.inject(UserService);
  const httpTesting = TestBed.inject(HttpTestingController);

  // 1. メソッドを呼ぶ(リクエストは偽の通信層に溜まる)
  let result: string[] = [];
  service.getUsers().subscribe((users) => (result = users));

  // 2. 「このURLに1件飛んだはず」と捕まえる
  //    (0件でも2件でもテストは失敗する)
  const req = httpTesting.expectOne('/api/users');

  // 3. 偽のレスポンスを返す → subscribe の中が動く
  req.flush(['田中', '鈴木']);

  // 4. 結果を検証
  expect(result).toEqual(['田中', '鈴木']);
});

3POSTリクエストの中身まで検証する

req.request にはメソッドや送信ボディが入っています。「正しいURLに、正しい中身で送っているか」まで検証できるので、送信系の処理こそHTTPテストの価値が出ます。

it('ユーザー登録は正しいボディでPOSTされる', () => {
  const service = TestBed.inject(UserService);
  const httpTesting = TestBed.inject(HttpTestingController);

  service.register('田中', 30).subscribe();

  const req = httpTesting.expectOne('/api/users');
  // メソッドの検証
  expect(req.request.method).toBe('POST');
  // 送信ボディの検証(オブジェクトなので toEqual)
  expect(req.request.body).toEqual({ name: '田中', age: 30 });

  req.flush({ id: 1 });
});

4エラーレスポンスの処理をテストする

flush にステータスコードを添えれば、サーバーエラーも自由に再現できます。本物のサーバーを落とさなくても「500が返ったらエラーメッセージを出す」という異常系を確実にテストできます。

it('サーバーエラー時はエラー用の処理が動く', () => {
  const service = TestBed.inject(UserService);
  const httpTesting = TestBed.inject(HttpTestingController);

  let errorMessage = '';
  service.getUsers().subscribe({
    next: () => {},
    error: () => (errorMessage = '取得に失敗しました'),
  });

  // 偽の500エラーを返す(本物のサーバーは不要)
  const req = httpTesting.expectOne('/api/users');
  req.flush('server error', {
    status: 500,
    statusText: 'Internal Server Error',
  });

  expect(errorMessage).toBe('取得に失敗しました');
});

5afterEach の verify で検証漏れを防ぐ

verify は「捕まえていないリクエストが残っていないか」を確認します。afterEach に置けば全テストで自動的に検証され、意図しない二重リクエストなどのバグを取りこぼさなくなります。

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

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

  afterEach(() => {
    // 各テストの最後に「未処理のリクエストが残っていないか」を確認。
    // 想定外のAPI呼び出し(二重リクエストなど)があると失敗して教えてくれる
    httpTesting.verify();
  });

  it('...', () => {
    // 各テストは expectOne / flush の流れを書くだけ
  });
});