背景
REST APIの開発に携わり、設計→実装→テスト→修正のループが回り始めたところで「テスト自動化したいな」と思い至りました。
- 実装しながら設計の微修正が入りがち(実際に作ってみることで設計のおかしさに気づくことがあるため)
*ちょくちょく変更が入ると、今何が正なのか混乱してくるので、やはり「テストケース」という基準があることが分かりやすくて良い、と思い始める - 実装期間の前半、後半のタイムラグ(前半に実装済みの機能が、後半の実装の何かによってデグレードすることが時々ある)
*個々の機能を作ってテストしたら終わりではない。マメにリグレッションテストしないとデグレっていないか心配 - 何度も同じテストを打鍵していると、かなり飽きる(飽きると手を抜き始めるので危険)
*そろそろ潮時なので、自動化に取り組んでみようと決意
ツール(手段)の選定
世の中には、テスト自動化ツールと呼ばれるものは無数にありそうです。
まずは以下のような要件を洗い出してみました。
(must要件)
- REST APIの呼び出しができること(GUIは不要)
- APIレスポンスのアサーションができること
- 無償で利用できること
(want要件)
- テスト開発にあまり学習コストがかからないこと
- 導入、運用など簡単に使えること
- テストの拡張ができること
must要件を満たすようなツールは、思ったよりたくさんありそうで、到底選べません。
頼みのwant要件は、ふわっとし過ぎていて絞り込みに役立たない・・・
そもそもテストの言語は何がいいのか?も迷ってしまいました。
今回はAPIをnode.jsで開発しているので、jsベースのツールがいいのだろうか?とも思いつつ、
アプリとテストは完全に独立しているので、有力なツールがあるならJavaやpythonでもいいよね・・・といった感じで、選定要件がさだまりませんでした。この調子だと永遠に決まらないので、この度はえいやっと見つけたツール、Frisby.jsを使ってみることにしました。
FrisbyはJestというJavaScriptのテストフレームワークをベースに構築されているツールで、APIエンドポイントのテストに特化しているようです。
Frisbyの使い方
導入
テスト作成用の任意のディレクトリを準備し、ディレクトリ配下でnpmでfrisbyとjestをインストール
npm install --save-dev frisby
npm install --save-dev jest
テスト用フォルダとファイル作成
frisbyとjestをインストールしたディレクトリに「__test__」という名称のフォルダを作成します。
この__test__フォルダ配下に、実行したいテストケースを作成していきます。
※フォルダ名は必ずこの名称にする必要があります。ツールが、__test__ という名前のフォルダ以下にあるテストを実行するものと認識しているようです
テストは「XX.spec.js」(XXの部分は任意)というファイル名で作成していきます。
※テストのファイル名のうち、末尾の .spec.js の部分は必ずこの名称にあわせる必要があります。
最終的に、こんな感じのディレクトリ構成になります
テスト用任意ディレクトリ/
├ __test__/
│ ├ api1_1.spec.js
│ ├ api1_2.spec.js
│ └ ・・・(以下同様に、必要なテストケースを量産していく)
├ node_modules/
├ package.json
└ package-lock.json
テストケースの実装
各XX.spec.jsファイルの中身を実装していきます。
以下は、getメソッドでリクエストを実行し、ステータスコードが200であることを確認するシンプルなテストケースの例です。
const frisby = require('frisby');
it('should be a teapot', function () {
return frisby.get('http://localhost:4000/api/documents/92_article2' ,{
headers: {
'search_app_authorization':'xxxx'
}
})
.expect('status', 200)
});
テストケースの実行
まず、package.jsonにscriptの定義を行います。
"scripts":{ "test:"jest" }
を追記します。
"scripts": {
"test": "jest"
},
"devDependencies": {
"frisby": "^2.1.3",
"jest": "^28.1.3"
}
}
その上で、テスト用ディレクトリで以下コマンドを実行します。
npm test
テスト実行が開始し、コンソール上に結果が表示されます。
以下、実行結果の例です。(2つのテストケースが実行され、両方ともpassしました)
>npm test
> test
> jest
PASS __test__/api_1_1.spec.js
PASS __test__/api_1_2.spec.js
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.71 s
Ran all test suites.
__test__フォルダ配下にあるテストケースがすべて実行されます。
テストの拡張
ここまでが基本的なテストの作り方と実行方法でした。
さらに今回のテストの内容に合わせて、以下のような拡張を行いました。
・環境バリエーションへの対応
・検証ポイントの追加
・タイムアウト値の変更
環境バリエーションへの対応
REST APIの開発とUTはローカルPC環境で実施しますので、ローカル環境(例:localhost:4000)でテスト実行できることが1つ目の要件です。さらに開発フェーズの後半では、チームで用意したサーバー環境にアプリをデプロイし、統合テストに向けた準備を進めていきますので、IT環境(例:it-server:4001)においても同じテストケースを再利用できることが望ましいです。
このように対象環境が異なっても、同じテストケースを実行できるようにする必要があり、リクエストURLに含めるホスト名をハードコードせず、configファイルで共通化する対応を行いました。別途、config.jsonに共通のHOST値を定義しておき、各テストケースでは${config.HOST}のように変数で記述します。こうすることで、テスト対象の環境が変わっても、config.jsonを1か所修正するのみでテストの再利用が可能になりました。
const frisby = require('frisby');
const config = require('../config.json');
it('should be a teapot', function () {
return frisby.get(`${config.HOST}/api/documents/92_article2` ,{
headers: {
'search_app_authorization':'xxxx'
}
})
.expect('status', 200)
});
検証ポイントの追加
各テストにおける検証ポイントとして、HTTPステータスコードが期待通りであること以外にも、レスポンスデータの中身をチェックしたいケースがありました。
frisbyであらかじめ用意されているアサーションの機能もいくつかありますが、もっと各自の要件に合わせてカスタマイズしたい場合は、JavaScriptのコーディングにて拡張することもできそうです。(jestで用意されている機能も幅広くありそうなので、そちらの仕様をまずは確認するのが良さそうです)
以下の例では、
.then(function (res) { ・・・})
の部分で、REST APIからのレスポンスデータをresで受け取り、自由に加工することができます。
その後
expect(XX).toBe(YY)
という形式で、XXがYYに等しいかどうかを検証することができます。(jestの仕様を確認すると、他にもさまざまなメソッドが準備されています)
const frisby = require('frisby');
const config = require('../config.json');
it('should be a teapot', function () {
return frisby.post(`${config.HOST}/api/projects/query` ,{
headers: {
'Content-Type': 'application/json',
'search_app_authorization':'xxxxx'
},
body:{
"query":"sample"
}
})
.expect('status', 200)
.then(function (res) {
json = JSON.parse(res.body);
expect(json.matching_results).toBe(14);
}
)});
タイムアウト値の変更
jestの仕様として、タイムアウトのデフォルト値が5000ミリ秒となっているようです。
今回開発したREST APIの一部は、想定として応答時間が5秒以上かかるものであるため、そのままでは毎回タイムアウトでテストが失敗してしまうことが分かりました。
そこで、応答時間が長いと分かっているテストケースについては、タイムアウト値をあらかじめ延ばしておく対応を行いました。
以下の例のように、globalSetupを呼び出して、値を変更することができるようです。
const frisby = require('frisby');
// テストのタイムアウトを10秒に変更
frisby.globalSetup({
request: {
timeout: 10000
}
});
残課題
APIのテストを自動化したい、という当初の想定は凡そクリアすることができて、ここまでの内容については、学習コストもそこまでかからなかったので良かったと思います。(jestを今まで使ったことはなかったのですが、とても汎用的なフレームワークだと思われ、情報源も多くありそうなのが良い点でした。言語がJavaScriptベースというのも、汎用性が高くて良いと思いました)
ただし、今回自動化できたのは、正常系の基本パターンのテストの範囲のみであり、本来のテストケースの「完全自動化」に向けてはまだ壁が残っているので、今回できなかったことを備忘録としてメモっておきます
- DBのチェック
API実行時にDBテーブルへの書き込み・削除を伴うケースがあるので、DB側の処理が正しく行われたかどうかの確認もしたい(現状は目検) - 異常系ケースの前後処理の自動化
異常系のテストケースも自動化する場合は、エラーを意図的に発生させるための手順を、テストケースの前後に仕込む必要がある(設定ファイルの書き換え、サーバーの落とし上げなど)
また、各テストケースの実行順序の制御が必要な場合は、その方法も検討する必要がある。(frisbyをノーマルに実行すると、__test__フォルダ配下にあるテストケースを並列に実行しているように見えており、実行順序は担保されない) - redirectのAPIのステータスコードチェック
redirectの場合、HTTPステータスコードが301になることを確認したかったのですが、そのままだとリダイレクト先のステータスコードが返って来てしまうので、何か一工夫が必要そうでした。(こちら、時間切れにて未着手)
参考情報