Frisby.js v0.x を v2.x にしたハナシ
検索プラットフォーム事業部の田中です。
フォルシアでは、最新の技術をプロダクトに取り込むということにも果敢に挑戦していますが、一方でレガシーコードの改善や日々の運用の改善にも力を入れています。
今回は、過去にAPIテストを自動化するための Frisby.js のバージョンが0.85と古くなっていたため、今更ですが最新の2.xに置き換えた話をします。
Frisby.js とは
APIのブラックボックステストを自動化するためのフレームワークで、JavaScriptの定番のテストフレームワークのJestベースで書かれています(2.0の途中でJasmineからJestに変更になりました)。APIテストフレームワークのデファクトになりつつある人気のツールです。
最初のコミットは2011年と長く愛されています。ながらくv0.85からリリースがありませんでしたが、1系をとばして2017年の7月にv2.0がリリースされました。
v0.x からv2.x へ
変更点についてはこちらにもありますが、独自のラッパーを書くのではなく、Jest(またはJasmine)の作法に則る形でテストを書けるようになりました(it -> expect で書けるようになった)。
より詳細な、変更点や思想はメインコミッターのブログの記事を読むと良いかと思います。
v0.x のコード
var frisby = require('frisby'); frisby.create('should get user Joe Schmoe') .get(testHost + '/users/1') .expectStatus(200) .expectJSON({ id: 1, email: 'joe.schmoe@example.com' }) .toss();
v2.x のコード
var frisby = require('frisby'); it('should get user Joe Schmoe', function() { return frisby.get(testHost + '/users/1') .expect('status', 200) .expect('json', { id: 1, email: 'joe.schmoe@example.com' }); });
基本的にはJestの文法に従って、frisby->create->get->expectXX->toss を frisby->get->expect('XX') と変えていくだけです。
型のチェックは、JavaScript の型を指定していた箇所が Joi のオブジェクトを指定するように変わっています。 これにより、さらに細かいバリデーションができるようになっています。
v0.x
expectJSONTypes('*', { hoge: String })
v2.x
expect('jsonTypes', '*', { hoge: Joi.string() })
また v2.x から expectJSON(...) の代替として expet('json', ...) と expect('jsonStrict', ...) がありますが、json は指定したものが一致していれば良い jsonStrict は完全に一致している必要があります。
移行時に実際に困ったところ
基本的には、v0.x と v2.x は同等のものが用意されており置き換えていくだけですが、いくつか考慮が必要だった点があるので記載します。
配列同士の比較が厳密な比較になってしまう
例えば下記のようなレスポンスがあったとします。
[ { hoge: 1, fuga: 2 } ]
v0.x では下記でテストをパスしていました。
expectJSON( '*', [ { hoge: 1 } ] )
v2.x では下記はFailになってしまいます。
expect( 'json' '*', [ { hoge: 1 } ] )
下記のように指定する必要があります。
expectJSON( '*.0', { hoge: 1 } )
expectJSON でできていた関数の評価ができない
v0.x では expectJSON の中で下記のように評価関数を実行することができました。
expectJSON( { message: function(val) { expect(val).toBe('hoge'); } } )
v2.xではこれができなくなっています。そこでかなり力技ですが、下記のように再帰的に評価するようなカスタムマッチャーを作ればこれを実現できます。
const getPathVal = (response, prefix, results) => { results = results || []; for (let key in response){ const path = prefix ? prefix + '.' + key : key; if (typeof response[key] === 'object') { if(Array.isArray(response[key])) { response[key].forEach((item, i) => { getPathVal(item, path + '.' + i, results); }); } else { getPathVal(response[key], path, results); } } else { results.push({path: path, val: response[key]}); } } return results; }; beforeAll(() => { frisby.addExpectHandler('jsonf', (response, path, expected) => { frisby.utils.withPath(path, response.json, (jsonChunk) => { getPathVal(expected).forEach((item) => { frisby.utils.withPath(item.path ,jsonChunk, (actual) => { // valがfunctionの場合はmatcherが設定されているのでそれを評価する if(typeof item.val === 'function') { item.val(actual); } else { expect(actual).toBe(item.val); } }); }); }); }); }); frisby.get(host).expect( 'jsonf', '*', { message: function(val) { expect(val).toBe('hoge'); } } );
ちなみにこのカスタムマッチャーを定義しておくと、配列同士の比較が厳密な比較になってしまう問題も解決できます。
フォームデータ形式のpostリクエストの指定の仕方が変わった
v0.x ではPOSTでリクエストを送る場合は引数にオブジェクトを指定すれば、よしなに body を application/x-www-form-urlencoded の形式に変換して送ってくれていたのですが、v2.0 では下記のようにする必要があります(formDataで指定もできますがここでは割愛します)。
const body = { hoge: 1, fuga: 2 }; const frisby = require('frisby'); const qs = require('qs'); frisby.setup({ request: { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } ).post(host, qs.stringify(body)).;
さいごに
すごく地味な作業ですが、こういったツール類も新しいものに移行していくと、新機能を使えるなど色々と恩恵を受けられます。最初からバージョンアップを見越して、テストコードにロジックを書き過ぎないようにしたり、正解データをコードとは切り離して持っておいたりすることなどが重要です。ちょっと重い腰を上げてバージョンアップしていきましょう。
田中 謙次
技術部長。メーカの研究所を経て、2013年からフォルシアに参画。
アジャイル開発をベースに開発プロセスの改善を担当。
改善のためなら手段は厭わないポリシー。