サービスの単体テスト(2)
ひな型をベースに単体テストを記述
リスト3のひな型をもとにitメソッドを増やして、テストを記述できます。サンプルコードではリスト4のような実装を複数記述しています。さまざまな引数のパターンでgetGreetメソッドを実行して、戻り値greetが想定通りになっているか、expect(greet).toEqualメソッドで確認します。
it('日本語:朝1', inject([GreetService], (service: GreetService) => { const greet = service.getGreet('太郎', new Date(2005, 3, 1, 0, 0, 0, 0), 'ja-JP'); expect(greet).toEqual('おはようございます, 太郎'); }));
テストの実行
Angular CLIで「ng test」コマンドを実行すると、単体テストが実行されて、実行結果がChromeブラウザーに表示されます。図3はすべてのテストが成功した表示です。
コマンドを実行中の状態で、GreetServiceや単体テストの実装を編集すると、テストが自動的に再実行されます。図4は、編集でテストが失敗するようになった表示です。
以上の通り「ng test」コマンドを利用すると、リアルタイムで単体テストの結果を確認しながら実装を進められます。
HTTP通信を含むサービスをモックでテスト
ネットワーク通信でAPIからデータを取得するといった、外部との通信を伴うサービスをテストする場合、通信処理をモック(本物のクラスの代わりに、テスト用の値を返却するクラス)で置き換えると便利です。通信処理を含む図5のサンプル(P002-mock)で、モックの利用法を説明します。テキストボックスに入力された検索語でWikipediaを検索して、検索結果のJSONを画面に表示します。
Wikipediaを検索するWebApiServiceをリスト5に示します。
@Injectable() export class WebApiService { // 依存性注入でHttpClientを受け取る ...(1) constructor(private myClient: HttpClient) { } // Wikipediaを検索 ...(2) searchWikipedia(query: string) { return this.myClient.jsonp( // JSONPで通信実行 ...(3) 'http://ja.wikipedia.org/w/api.php?format=json&action=query&list=search&srsearch=' + encodeURIComponent(query), 'callback') .pipe( map(p => p['query']['search'])// 必要なノードを抽出 ...(4) ); } }
(1)のコンストラクターで、HTTP通信を行うHttpClientのインスタンスを依存性注入で受け取り、(2)のsearchWikipediaメソッドでHttpClientを利用してAPIを実行します。Webページとオリジン(ドメインとポート番号)が異なるURLのAPIを呼び出すため、(3)のようにjsonpメソッドでJSONP通信をさせます。API結果が格納されたJavaScriptオブジェクトpから、(4)で必要なノードを抽出します。
コンポーネント側では、サービスから返却されたRxJSのObservableを購読(Subscribe)して(4)で抽出したノードを取り出し、画面に反映します。実装の詳細はサンプルコードを参照してください。
HttpClientのモックを作成して利用
モックを利用してリスト5のサービスをテストするコードを、リスト6に示します。
// テストデータ ...(1) const expectedValue = [ { 'title': 'test1' }, (略) ]; // API結果のデータ構造と合致するように調整 ...(2) const dummyResponse = { 'query': { 'search': expectedValue } }; // テスト初期化処理 ...(3) beforeEach(() => { // jsonpメソッドで固定値を返却するHttpClientのモックを作成 ...(4) const httpClientSpy = jasmine.createSpyObj('HttpClient', ['jsonp']); httpClientSpy.jsonp.and.returnValue(asyncData(dummyResponse)); // (4)のモックを利用するサービスを生成 ...(5) const testService = new WebApiService(httpClientSpy); TestBed.configureTestingModule({ providers: [ // テスト対象サービスを(5)のサービスに入れ替え ...(6) { provide: WebApiService, useValue: testService } ] }); }); // テスト処理 ...(7) it('モックでHttpClientを模擬', inject([WebApiService], (service: WebApiService) => { service.searchWikipedia('someQuery') .subscribe(v => { expect(v).toEqual(expectedValue); // テストデータが返却される ...(8) }); }));
(1)がモックのテストデータです。HttpClientのモックから返却される値は、WikipediaのAPIで取得できるデータと構造を合わせて、(2)のように調整します。
(3)がテストの初期化処理です。(4)のjasmine.createSpyObjメソッドで、HttpClientのモック(Spyオブジェクト)を作成します。メソッドの第1引数にモックの名前、第2引数にモックを作るメソッド名配列(ここでは「jsonp」1つだけ)を指定します。作成したモックhttpClientSpyのjsonp.and.returnValueメソッドで、jsonpメソッド実行時に(2)のdummyResponseを含むObservableが返却されるように指定します。asyncDataは、購読されたときに指定データを返却するObservableを生成する処理です。詳細はサンプルコードを参照してください。
(5)で、本物のHttpClientの代わりに(4)のモックをコンストラクターに与えてWebApiServiceを生成します。TestBed.configureTestingModuleメソッド内で、providersを(6)のように記述すると、テスト実行時に(5)のtestServiceが依存性注入されるようになります。「useValue」は、依存性注入されるオブジェクトを指定するオプションです。
テスト処理は(7)です。依存性注入で取得されるWebApiService型の変数serviceは、実は(5)のtestServiceなので、searchWikipediaメソッドの内部でHttpClientのモックからデータを取得して、最終的に取得される値はexpectedValueと一致します(8)。