はじめに
前々回の【AWS】単体テストを考慮したLambdaの構成を考えたで、単体テストについて少し触れました。
で、今回はそこから一歩踏み込んで「実際にテストを導入してみよう!」という内容になります。(なぜ前回書かなかったのかは置いといて)
また、どうせならServerless Framework経由で利用できるように「Serverless Jest Plugin」という、Serverless Frameworkのプラグインにて行う内容になっています。
Serverless Jest Pluginとは?
名前の通り、Serverless Framework上でJestを使用したテスト(テスト駆動開発:TDD)を行うためのプラグインになります。
www.serverless.com
ちなみに(今回は扱いませんが)、上記のMocha版である「Serverless Mocha Plugin」というのもあります。
www.serverless.com
このServerless Jest Pluginを使用して、以下のことができます。
- テストの作成
- テストの実行
インストール&設定
インストールですが、他のプラグイン同様、以下を行うだけです。
- npm installの実施
- serverless.ymlの「plugins」セクションに追加
# npm installを実施する > npm i serverless-jest-plugin --save-dev # serverless.ymlのpluginsセクションに追加する plugins: - serverless-jest-plugin
また「custom」セクションに「jest」というキーを設定し、その子要素に「Jestの設定項目:その値」というKey:Valueを用意することで、その項目の設定をそのまま反映することができます。
# 設定例 # (例)下記の記載をすると、Jestのテスト時間を30秒に延ばすことができる。(デフォルトは5秒) custom: jest: testTimeout: 30000
テストの作成
で、まず行う「テストの作成」ですが、これは下記コマンドを実施すればOKです。
# Lambda関数作成と同時にテストを作成する場合 > sls create function -f myFunction --handler ./index.handler --path tests # 既存のLambda関数に対して、テストだけ作成する場合 > sls create test -f myFunction--path tests
パラメータの意味は、以下の通りです。
パラメータ | 意味 | 設定値 | 備考 |
---|---|---|---|
-f | Lambda定義名 | serverless.ymlのfunctionsセクションに定義する(した)Lambda定義名 | |
--handler | ハンドラ関数名 | Lambdaのハンドラ関数のパス(ファイル名&関数名) | serverless.ymlのfunctionsセクションのhandlerに設定される値 |
--path | テストファイルを格納するフォルダのパス | ・未指定の場合、「__tests__」というフォルダを自動作成し、その中に格納する。 ・「{function}」という値を指定すると、フォルダは作成せず、元のLambda関数と同じ階層にテストファイルを作成する |
「{function}」は、実際のLambda定義名に置き換える...などではなく、「{function}」という固定の文字列 |
例えば、以下のコマンドを実行した場合、結果は下記の通りになります。
> sls create function -f pow--handler ./pow.index --path {function}
# serverless.ymlのfunctionsセクションに、以下の定義が追加される functions: pow: handler: ./pow.index
フォルダ:
- <ルートフォルダ>
- node_modules
- pow.js
- pow.test.js
また、作成されたテストファイル(pow.test.js)の中身は下記の通りです。
'use strict'; // tests for pow // Generated by serverless-jest-plugin const mod = require('./pow'); const jestPlugin = require('serverless-jest-plugin'); const lambdaWrapper = jestPlugin.lambdaWrapper; const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' }); describe('pow', () => { beforeAll((done) => { // lambdaWrapper.init(liveFunction); // Run the deployed lambda done(); }); it('implement tests here', () => { return wrapped.run({}).then((response) => { expect(response).toBeDefined(); }); }); });
テストの実行
もう一つの「テストの実行」ですが、これは下記コマンドを実施すればOKです。
> sls invoke test -f pow--stage dev --region ap-northeast-1
パラメータの意味は、以下の通りです。(すべて任意項目です)
パラメータ | 意味 | 設定値 | 備考 |
---|---|---|---|
-f | テストするLambdaの定義名 | serverless.ymlのfunctionsセクションに定義したLambda定義名 | 未指定の場合、全テストが実施される |
--stage | 実施するステージ | Lambdaのテストを実施するステージを指定したい場合、設定する | 不要なら未指定でもOK |
--region | 実施するリージョン | Lambdaのテストを実施するリージョンを指定したい場合、設定する | 不要なら未指定でもOK |
実際に先程のテスト(pow.test.js)を実施すると、以下の結果になります。
また、下記のようにテストを変更すると、ちゃんとテスト結果はfailになります。
// pow.test.jsに下記テストを追加する it('statusCode test', () => { return wrapped.run({}).then((response) => { expect(response.statusCode).toBe(200); }); }); // pow.jsのハンドラ関数(index)を下記の通りに変更する module.exports.index = (event, context, callback) => { const response = { statusCode: 400, // statusCodeを200→400に変更する body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }), }; callback(null, response); };
その他の事項
で、その他の事項ですが...公式ページのドキュメントに書いてあるのは、これで以上です。
おそらくは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんででしょう。
なので、簡単な使い方を説明するのは以上ですが、ちょっと調査したことを羅列したいと思います。
ローカル関数のテストは可能?
結論から言うと「可能」です。
ただし、ローカル関数が以下の条件を満たす場合のみです。
- exportされている(まあこれは当然)
- 非同期関数(async function)である
例えば、pow.jsを以下の通りに変更します。(event.numの2乗の数値をbody.powResultに設定してレスポンスを返すLambda関数です。)
'use strict'; const index = async (event, context, callback) => { const powResult = await main(event.num); const response = { statusCode: 200, body: JSON.stringify({ message: 'Go Serverless v1.0! Your function executed successfully!', input: event, powResult: powResult }), }; callback(null, response); }; const main = async num => { return await funcPow(num); } const funcPow = async num => { return Math.pow(num, 2); } module.exports = { index: index, main: main, funcPow: funcPow }
で、pow.test.jsに下記テストを設定します。
'use strict'; // tests for pow // Generated by serverless-jest-plugin const mod = require('./pow'); const jestPlugin = require('serverless-jest-plugin'); const lambdaWrapper = jestPlugin.lambdaWrapper; const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' }); const wrappedfuncPow = lambdaWrapper.wrap(mod, { handler: 'funcPow' }); // これを追加 it('main Function test', async () => { // ローカル関数funcPowのテスト const result = await wrappedfuncPow.run(4); expect(result).toBe(16); });
上記テストを実施すると、ちゃんとローカル関数funcPow()のテストができます。
しかし、以下のようにfuncPow()を同期関数に書き換えて(「async」を削除)テストを実施すると...
// pow.jsのfuncPowを同期関数に変更する const funcPow = num => { console.log('funcPow Called'); return Math.pow(num, 2); } // pow.test.jsでfuncPowを呼ぶ it('funcPow Function test', () => { const result = wrappedfuncPow.run(4); expect(result).toBe(16); });
結果、テストは失敗します。
というか、戻り値が空オブジェクトになっています。
また「funcPow Called」というログが出力されているので、funcPow関数自体は呼ばれていることが分かると思います。
これですが、元となるlambdaWrapperに下記のような処理がなされているため、戻り値がPromiseでないと、正しく処理されないためです。
if (this.handler) { const handlerResult = this.handler(event, lambdaContext, callback); // Check if the result itself is a promise if (handlerResult && handlerResult.then) { handlerResult.then(function(data) { // Avoid Maximum call stack size exceeded exceptions return setImmediate(function() { callback(null, data); }); }).catch(function(err) { // Avoid Maximum call stack size exceeded exceptions return setImmediate(function() { callback(err); }); }); } } else { // 以下略 }
この場合は、無理やりlambdaWrapper.wrapを使わず、素直に以下のような、Jest標準の記載をすればOKです。
// pow.jsのfuncPowを同期関数に変更する const funcPow = num => { console.log('funcPow Called'); return Math.pow(num, 2); } // pow.test.jsでfuncPowを呼ぶ it('funcPow Function test', () => { const result = mod.funcPow(4); expect(result).toBe(16); });
この点からも、Serverless Jest Pluginは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんでしょう。
まとめ
というわけで、Serverless Jest Pluginについて簡単に説明しました。
決して「これ一つで劇的にテスト環境が改善する」とか「至れり尽くせり」ではないですが、Lambdaテスト導入の第一歩としては、悪くないツールだと思います。
こういうツールをきっかけに、テストについてある程度詳しくなって、そこから次のステップに進むのが良いのかな、と個人的に感じました。
それでは、今回はこの辺で。