[Angular, Jest] 依存している他のサービスをモック化してサービスクラスをテストする方法
はじめに
AngularのサービスクラスのふるまいをJestを使ってテストするテストコードについて紹介します。
サービスクラスは他のサービスクラスに依存することが多いと思うので、他のサービスクラスをモック化する方法も説明します。(むしろモック化する方法がメインになっています。)
なお、Jestのインストール方法などは割愛します。jest-preset-angularなどを用いてセットアップしてください。
環境
Node: 12.16.1
TypeScript: 4.0.5
Angular: 10.1.6
Jest: 26.6.2
jest-preset-angular: 8.3.2
テストするサービスクラス
Angular公式ドキュメントに出てくるチュートリアル Tour of Heroesの「4. サービスの追加」で出てくるHeroServiceをテストしたいと思います。
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
}
このHeroServiceはMessageServiceに依存しており、add
メソッドを呼び出しています。
UTの範囲ではMessageServiceについて考える必要がないのですが、MessageServiceは以下です。
MessageService
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
テスト観点
今回の記事では、以下のようなテスト観点を満たすようにテストコードを作成します。
- HeroServiceがインスタンス化されていること
- 依存クラス(MessageService)をモック化できていること
-
HeroServiceクラスの
getHeroes
メソッドが想定通りの値を返却していること -
MessageServiceの
add
メソッドが呼ばれていること
テストコード
上記のテスト観点をすべてクリアするテストコードは以下です。(次節で解説します)
import { TestBed } from '@angular/core/testing';
import { HeroService } from 'src/app/hero.service';
import { MessageService } from 'src/app/message.service';
import { HEROES } from 'src/app/mock-heroes';
// 外部モジュールのモック化 (describeより前に記述すること)
jest.mock('src/app/message.service');
describe('TestHeroService', () => {
let heroService: HeroService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
heroService = TestBed.inject(HeroService);
});
describe('テスト準備', () => {
it('HeroServiceがインスタンス化されていること', () => {
expect(heroService).toBeTruthy();
});
it('依存クラス(MessageService)がモック化されていること', () => {
const mockedMessageService = TestBed.inject(MessageService) as jest.Mocked<MessageService>;
expect(mockedMessageService.add.mock).toBeTruthy();
});
});
describe('getHeroesメソッドのテスト', () => {
it('モック用の配列を返却すること', () => {
// 準備
const expectedResult = HEROES;
// 実行
const result$ = heroService.getHeroes();
// 検証
result$.subscribe(result => {
expect(result).toEqual(expectedResult);
});
});
it('MessageServiceのaddメソッドを呼び出していること', () => {
// 準備
const messageService = TestBed.inject(MessageService);
const spy = jest.spyOn(messageService, 'add');
// 実行
heroService.getHeroes();
// 検証
expect(spy).toBeCalled();
});
});
});
テストコード解説
HeroServiceがインスタンス化されていること
HeroServiceのテストを実施するにあたり、HeroService
をインスタンス化する必要があります。
ユニットテスト用にコンポーネントやサービスを作成するTestBed
というクラスがAngularのcoreモジュールに用意されているのでこれを利用します。
let heroService: HeroService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
heroService = TestBed.inject(HeroService);
});
TestBed.configureTestingModule
でテスト用モジュールでの設定を行うことができます。後ほど説明しますが、ここではMessageService
をproviders
に追加しています。
そして、TestBed.inject(HeroService)
によってインスタンス化されたHeroService
を取り出すことができます。
beforeEach
で毎回取得することで、テストごとにインスタンス化され、あるテストでの操作が他のテストに影響することがありません。
よって、以下のテストケースが通ります。
it('HeroServiceがインスタンス化されていること', () => {
expect(heroService).toBeTruthy();
});
依存クラス(MessageService)をモック化できていること
次に、HeroService
が依存しているMessageService
がモック化できていることを確認します。
ポイントは以下の2つです。
-
jest.mock(モジュールのパス)
で依存クラスをモック化する -
TestBed.configureTestingModule()
でproviders
に依存クラスを追加する
jest.mock()
を使用すると、依存クラスのすべてのメソッドをモック関数に変更したクラスでオーバーライドされます。モック関数はデフォルトでは常にundefined
を返却します。(参考: ES6 Class Mocks - Jest)
今回では、
jest.mock('src/app/message.service');
を実行し、'src/app/message.service'
でexportされているMessageService
クラスを自動的にモック化します。jest.mock
はdescribe
よりも前に書く必要があるので注意です。
これにより、このファイル(hero.service.spec.ts
)の範囲では、MessageService
クラスはモッククラスで自動的にオーバーライドされるようになりました。
次に、TestBed.configureTestingModule()
でproviders
にMessageServiceクラスを指定すると、モッククラスを利用するようにモジュール定義を設定することができます。
jest.mock
で自動モックしていない場合は、通常のMessageServiceクラスが利用されます。
最後に、モック化されていることを確認します。
it('依存クラス(MessageService)がモック化されていること', () => {
const mockedMessageService = TestBed.inject(MessageService) as jest.Mocked<MessageService>;
expect(mockedMessageService.add.mock).toBeTruthy();
});
TestBed.inject(MessageService)
によってMessageService
のインスタンスを取得します。inject
メソッドの返り値の型推論はMessageService
ですが、実際にはモック化されたMessageService
インスタンスが返ってくるので型を指定します。(この部分はテクニックが必要)
TestBed.inject(MessageService) as jest.Mocked<MessageService>;
モック関数に置き換わったadd
メソッドはmock
というプロパティを持つので、これを持つことでモック化されていることを確認できます。
expect(mockedMessageService.add.mock).toBeTruthy();
上記が、メソッドのテストを実行するための準備でした。
getHeroes
メソッドが想定通りの値を返却していること
HeroServiceクラスの今回は、getHeroes
メソッドがモック用の値HEROES
のObservable
を返却していることを確認します。
メソッドの返り値がObservable
型なので、subscribe
した後にexpect
で確認します。
it('モック用の配列を返却すること', () => {
// 準備
const expectedResult = HEROES;
// 実行
const result$ = heroService.getHeroes();
// 検証
result$.subscribe(result => {
expect(result).toEqual(expectedResult);
});
});
MessageServiceのaddメソッドを呼び出していること
依存先のクラスのメソッドを呼び出していることを確認したい場合があるでしょう。
その場合は、対象メソッドをスパイすることで確認することができます。
(スパイする=対象メソッドの実装を追跡できるようにすること。メソッドのふるまいは変えません。)
今回は、MessageService
のadd
メソッドが呼ばれているかどうか確認したいので、以下のようにspyオブジェクトを取得します。
const messageService = TestBed.inject(MessageService);
const spy = jest.spyOn(messageService, 'add');
このspy
オブジェクトを用いることで対象メソッドが呼ばれているか確認することができます。
it('MessageServiceのaddメソッドを呼び出していること', () => {
// 準備
const messageService = TestBed.inject(MessageService);
const spy = jest.spyOn(messageService, 'add');
// 実行
heroService.getHeroes();
// 検証
expect(spy).toBeCalled();
});
ちなみに、以下のようなコードでもspyすることができます。
const spy = jest.spyOn(MessageService.prototype, 'add');
まとめ
いかがだったでしょうか?
Angularでサービスクラスをテストする方法、依存先のクラスをモック化する方法を解説しました。
自分自身が依存先サービスクラスのモック化に苦戦したので、少しでも誰かの助けになれば幸いです。
Discussion