Angular の HttpClient を使うサービスクラスのテスト
最近、TypeScript の楽しさに気づきました。
HttpTestingModule
と HttpTestingController
を使ったテストの書き方です。
テスト対象を用意するために https://angular.jp/tutorial/toh-pt6 で公開されているプロジェクトを使います。
このプロジェクトの HeroService
をテストします。
概要
HttpClientTestingModule
と HttpClientTestingModule
を使えば、テスト対象のクラスの動きだけでなく、リクエストした内容が期待通りの結果になっているか検証できる。
公式が詳しいのでもっと知りたい人は、公式ドキュメントの HTTPリクエストのテスト を参照しよう。
準備
これ を落としてきて、任意の場所で展開します。
動作することを確認し、テストも動かして全て通ることを確認します。
hero.service.spec.ts
を作成し、以下のように書きます。
import { MessageService } from "./message.service"; import { HttpClient } from '@angular/common/http'; import { HeroService } from './hero.service'; describe('HeroService', () => { let httpClientSpy = jasmine.createSpyObj<HttpClient>(["get"]); let messageServiceSpy = jasmine.createSpyObj<MessageService>(["add"]); let service: HeroService; beforeEach(() => { service = new HeroService(httpClientSpy, messageServiceSpy) }); it('should be created', () => { expect(service).toBeTruthy(); }); });
テストが通っているか確認して、問題なければ次に進みましょう。
ちなみにここでやっているのは、サービスに必要な2つのインスタンス(httpClientSpy
, messageServiceSpy
)のスパイオブジェクトを作成し、そのスパイオブジェクトを引数にサービスを生成しています。
TetBed を使った方法に変える
引数で渡すのは面倒なので、TestBed
を使って生成するように変更します。
import { MessageService } from "./message.service"; import { HttpClient } from '@angular/common/http'; import { HeroService } from './hero.service'; import { TestBed } from '@angular/core/testing'; describe('HeroService', () => { let httpClientSpy = jasmine.createSpyObj<HttpClient>(["get"]); let messageServiceSpy = jasmine.createSpyObj<MessageService>(["add"]); let service: HeroService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: HttpClient, useValue: httpClientSpy }, { provide: MessageService, useValue: messageServiceSpy } ], }); service = TestBed.inject(HeroService); }); it('should be created', () => { expect(service).toBeTruthy(); }); });
変更箇所は、以下です。
import { HttpClient } from '@angular/common/http'; + import { TestBed } from '@angular/core/testing'; beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: HttpClient, useValue: httpClientSpy }, + { provide: MessageService, useValue: messageServiceSpy } + ], + }); + service = TestBed.inject(HeroService); - service = new HeroService(httpClientSpy, messageServiceSpy)
これも問題なく通るはずです。本題ではないので、説明は省きますが、何をやっているか分からない場合、以下を参照するのをオススメします。
HttpClientTestingModule と HttpClientTestingModule を使う
ここが今回の本題です。
以下のように変えます。
import { MessageService } from "./message.service"; import { HttpClient } from '@angular/common/http'; import { HeroService } from './hero.service'; import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; describe('HeroService', () => { let httpClient: HttpClient; let httpTestingController: HttpTestingController; let messageServiceSpy = jasmine.createSpyObj<MessageService>(["add"]); let service: HeroService; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ { provide: MessageService, useValue: messageServiceSpy } ], }); service = TestBed.inject(HeroService); httpClient = TestBed.inject(HttpClient); httpTestingController = TestBed.inject(HttpTestingController); }); it('should be created', () => { expect(service).toBeTruthy(); }); });
変更箇所は、以下です。
import { TestBed } from '@angular/core/testing'; + import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; describe('HeroService', () => { - let httpClientSpy = jasmine.createSpyObj<HttpClient>(["get"]); + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; ... providers: [ - { provide: HttpClient, useValue: httpClientSpy }, { provide: MessageService, useValue: messageServiceSpy } ... service = TestBed.inject(HeroService); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); });
とりあえずテストを通るか確認します。HttpClientTestingModule
, HttpTestingController
を使うとリクエストした内容の検証までできるようになります。次はそれを試すためにテストを追加します。
リクエストの内容を検証するテストを作る
以下のテストを追加して、通るか確認します。
it('サーバからヒーローを取得できる', async () => { const expected: Hero[] = [ { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, ]; service.getHeroes().subscribe(actual => { expect(actual).toEqual(expected); }); const req = httpTestingController.expectOne('api/heroes'); expect(req.request.method).toEqual('GET'); req.flush(expected); httpTestingController.verify(); });
解説
最初に期待値を作っています。この期待値は、同時にサーバから返ってくる値にもなります(req.flush(expected);
で行っています)。
そのあと、service.getHeroes()
を実行して、サブスクライブしています。データを受け取るとそのデータが期待通りになっているか検証しています(expect(actual).toEqual(expected);
の箇所です)。
この検証の処理は、まだ行われません。サーバからの応答は、HttpTestingController
で制御できます(厳密には、TestResult
?)。
先の処理で、リクエストは投げられたので、その内容が httpTestingController
で取得できます。const req = httpTestingController.expectOne('api/heroes');
がそれです。
expectOne
には URL を指定していますが、URL だけでマッチングさせるのが不都合な場合、独自に定義することができます。詳しくは、公式のドキュメントに委ねます。
expect(req.request.method).toEqual('GET');
は見たら分かる通り、GET や POST などの期待するメソッドになっているか検証しています。
req.flush(expected);
でサーバからの応答を擬似的に行っています。ここで指定した引数が actual
に入ってきます。この関数を抜けたあとにデータが流れて、expect(actual).toEqual(expected);
が行われるはずです。
最後に httpTestingController.verify();
で未処理のリクエストがないことを確認しています。 afterEach
で行うのが一般的みたいです。残っている場合は、テストが失敗します。
すごく雑な解説になりましたが、以上です。他にもエラーのテストや同じAPIの複数のリクエストなどの検証ができるみたいなので、知りたい方は、公式ドキュメントを参照してみるといいと思います。
最終的なコード
import { MessageService } from "./message.service"; import { HttpClient } from '@angular/common/http'; import { HeroService } from './hero.service'; import { Hero } from './hero'; import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpClientTestingModule } from '@angular/common/http/testing'; describe('HeroService', () => { let httpClient: HttpClient; let httpTestingController: HttpTestingController; let messageServiceSpy = jasmine.createSpyObj<MessageService>(["add"]); let service: HeroService; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ { provide: MessageService, useValue: messageServiceSpy } ], }); service = TestBed.inject(HeroService); httpClient = TestBed.inject(HttpClient); httpTestingController = TestBed.inject(HttpTestingController); }); it('サーバからヒーローを取得できる', async () => { const expected: Hero[] = [ { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, ]; service.getHeroes().subscribe(actual => { expect(actual).toEqual(expected); }); const req = httpTestingController.expectOne('api/heroes'); expect(req.request.method).toEqual('GET'); req.flush(expected); httpTestingController.verify(); }); });