Angularのテスト
公式サイトを見れば基礎的な内容はすべて書かれている。参考:Angular_リクエストのテスト
ポイントは、HttpTestingControllerを使用することで、通常のバックエンドではなく、テスト用のバックエンドにアクセスするようになること。
あとはこのテストコントローラーをどのように扱うか理解していれば実装できる。
flushとは何なのか
テストコードを書く上で一番理解しにくいのが、どのタイミングでHTTPクライアントの通信が実行されるのか?
通常のメソッドであればsubscribeやlastValueFromを呼び出したタイミングで実行されるが、テストの場合は厳密にはそうではない。
テストでは、リクエストの送信は通常通りのタイミングで行われるが、HTTPクライアントがレスポンスを返すのは、flushを呼び出すタイミングとなる。
リクエストをHttpTestingControllerに詰め込んで、flushで一斉にレスポンスをかえすイメージだ。
これがわかると、テストコードが書けるようになる。
HTTPクライアントのテスト
■service.ts(テストしたいメソッド)
HTTPリクエストで取得した文字列を2倍に連結して返すメソッドをテストする。
sampleFnc(): Observable {
const res = this.http.get('API_URL').pipe(
map((res) => {
retrurn res + res;
})
)
}
■service.spec.ts(テストコード)
flushに着目して、HTTPクライアントテスト時の通信タイミングを整理してみる。
// 事前準備
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
CompNameService, // テストで使用するサービス
// その他テストで使用するクラスを記述
],
imports: [HttpClientTestingModule]
}).compileComponents();
// DI構成
service = TestBed.inject(CompNameService);
httpTesting = TestBed.inject(HttpTestingController);
});
// テスト
it('sampleFnc_SUCCESS', () => {
// テストするメソッドのレスポンスを構成する
const res = lastValueFrom(service.sampleFnc()); // …①
// この時点で、HTTPクライアントに、リクエストは設定されるが、レスポンスはflushするまで返ってこない。
// HTTPリクエストの検証をするために、expect(検証内容)より前にHTTPクライアントにリクエストを設定しておく必要がある。
// httpTestingとexpectを使用して、検証する内容を構成する
const req = httpTesting.expectOne({ // HTTPクライアントにリクエストを送信する…②
url: 'API_URL',
method: 'GET'
});
res.then((resStr) => { // テストするメソッドから返されたres(…①)はPromiseなので、thenで値を取り出す…③
expect(resStr).toEqual(resMock + resMock); // resから取得した文字列が、テストメソッドの処理を通し、resMockを2倍に連結した値になっていることを確認する
})
// flushを実行することで、HTTPクライアントがレスポンスを返す。
// flushの引数にオブジェクトを指定すると、 リクエスト構成に対し任意のレスポンスを設定できる
resMock = 'テストレスポンス'; // "HTTPリクエスト"が返すレスポンスのMockを作成する
req.flush(resMock); // HTTPリクエスト(…②)の返すレスポンスをここで設定する。③の検証は、このタイミングで実行される。
httpTesting.verify(); // 未処理のリクエストが無いことを確認する
});
voidのテスト
■service.ts(テストしたいメソッド)
sampleFnc(): Observable {
return this.http.get('API_URL');
}
■service.spec.ts(テストコード)
it('sampleFnc_SUCCESS', () => {
const res = lastValueFrom(service.sampleFnc()); //この時点ではリクエストは実行されていない
const req = httpTesting.expectOne({
url: 'API_URL',
method: 'GET'
});
});
voidの場合、リクエストの検証ができればよいということであれば、flushする必要すら無い。
service.sampleFnc()を記述したタイミングで、httpTestingにリクエストがセットされているためである。
flushを記述するのは、レスポンスを検証したいから。
ただし、このとき、httpTesting.verify()を最後に記述するとエラーになるため注意する。
このメソッドはHTTPクライアントに、未完了のリクエストが無いかを検証しているので、レスポンスを返していない状態ではエラーとなる。
voidのレスポンスを検証したい場合は、リクエストの返却は以下のようにする。
req.flush({});
catchErrorのテスト
■service.ts(テストしたいメソッド)
sampleFnc(): Observable {
return this.http.get('API_URL').pipe(
catchError((err) => {
return of(err);
})
)
}
■service.spec.ts(テストコード)
catchError内の処理が実行されているかを検証するには、flushで、レスポンスにエラーを設定する。
it('sampleFnc_SUCCESS', () => {
const res = lastValueFrom(service.sampleFnc()).catch; // エラーをキャッチする
const req = httpTesting.expectOne({
url: 'API_URL',
method: 'GET'
});
// flushの引数で、レスポンスにエラーを設定する
req.flush('Failed!', {status: 500, statusText: 'Internal Server Error'});
expect(res).toThrowError; // エラーをスローしていることを検証する
});
httpTesting.verify();
ストア・クエリのテスト
■service.ts(テストしたいメソッド)
ストアに正しく値を設定できているかを確認したい。
setParam(val :string): Observable {
this.store.update({
storeParam: val
})
}
■service.spec.ts(テストコード)
describe('テストグループ名', () => {
let serviceName: ServiceName;
let serviceName: QueryName;
let serviceName: StoreName;
// 事前処理
beforeEach(() => {
TestBed.configureTestringModule({
providers: [
ServiceName,
QueryName,
StoreName
],
imports: [HttpClient TestingModule]
});
service = TestBed.inject(ServiceName); // サービスをテストモジュールに設定
query = TestBed.inject(QueryName); // クエリをテストモジュールに設定
store = TestBed.inject(StoreName); // ストアをテストモジュールに設定
// デフォルトとして用意しているテストデータがあれば以下記述
const masterMock = TestBed.inject(MasterDataMockService);
masterMock.initMockData();
});
// テスト実施
it('テスト名', () => {
const inputParam = new InputParam();
inputParam.param = 'sample'; // テストしたいデータを設定
serviceName.setParam(input Param); // テストしたいメソッド(setParam)を呼び出し>storeに格納されるので、queryから取得できるようになる
expect(query.getValue().inputParam.param).toBe('sample'); // クエリから取得したいが期待値と一致するか確認する
});
}
Cookieのテスト
ブラウザに設定されている’sample’という名前のcookieを取得し、リクエストヘッダーに設定するリクエストをテストしたい。
■service.ts(テストしたいメソッド)
sampleFnc(): Observable {
const httpOptions = {
headers: new HttpHeaders({
cookieSample: this.cookieService.get('sample');
})
}
return this.http.get('API_URL')
}
■service.spec.ts(テストコード)
// 事前にCookieを設定する
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
CookieService,
…
],
…
}).compileComponents();
…
cookieService = TestBed.inject(CookieService);
cookieService.set('sampleCookie', 'sampleCookieVal');
})
it('sampleFnc_getCookie_SUCCESS', () => {
lastValueFrom(service.sampleFnc());
const req = httpTesting.expectOne({
url: 'API_URL',
method: 'GET'
});
// beforEachで設定したCookieを、ヘッダーに設定できているかを確認する
expect(req.request.headers.get('sample')).toEqual('sampleCookieVal');
});
Appendix
テストの起動
npm test
ファイル単位でのテストの起動
npm run test --include src/[filePath]/….spec.ts --code-coverage
外部API・フレームワークのMock
郵便番号からの住所の取得や、QRコードの生成など、テストできない外部機能はMockを事前に作成してレスポンスを指定しておく。
以下、①はActivatedRouteをMock化してDI、②はHTMLCanvasElementが呼ばれたときにMockを返す方法。
beforeEach(() => {
const routerSpyObj = jasmine.createSpyObj('ActivatedRoute', ['snapshot']); // …①
const mockCanvas = jasmine.createSpyObj('HTMLCanvasElement', ['getContext']); // …②ダミーのHTMLCanvasElementクラスを作成。このクラスはgetContextメソッドを持つ
…
TestBed.configureTestingModule({
providers: [
{ provide: ActivatedRoute, useValue: routerSpyObj }, …①
…
]
}).compileComponents();
routerSpy = TestBed.inject(ActivatedRoute) as jasmine.SpyObj; // …①ActivatedRouteはMockとしてDIしておく
sptyOn(document, 'createElement').and.returnValue(mockCanvas); // …②documentクラスのcreateElementメソッドが呼ばれたときには、mockCanvasを返却する
})
テストのデバッグ方法
以下の設定を加える。
■karma.conf.js
module.exports=function(config){
config.set({
…
autoWatch: false,
browsers: ['Chrome'],
singleRun: false
})
}
■package.json
{
"scripts": {
"test": "--source-map=true"
}
}
IEモードで開発者ツールを使用する
参考:EdgeのIEモードの画面に対して開発者ツール(F12)を使う方法
用語集
- Jasmine:テストコードを効率よく記述するためのフレームワーク。Seleniumもこのレイヤーに位置する。
- Karma:テストランナー。テストコードをブラウザで起動して(この理由から、テスト中はchrome.exeのコンソールが開く)、レポートする。
コメント