echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】Serverless Jest Pluginで始めるLambdaテストの第一歩

はじめに

前々回の【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)を実施すると、以下の結果になります。
f:id:Makky12:20200816133241p:plain

また、下記のようにテストを変更すると、ちゃんとテスト結果は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);
};

f:id:Makky12:20200816133722p:plain

その他の事項

で、その他の事項ですが...公式ページのドキュメントに書いてあるのは、これで以上です。
おそらくは「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()のテストができます。

f:id:Makky12:20200816141353p:plain

しかし、以下のように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);
});

結果、テストは失敗します。
f:id:Makky12:20200820192818p:plain

というか、戻り値が空オブジェクトになっています。
また「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テスト導入の第一歩としては、悪くないツールだと思います。

こういうツールをきっかけに、テストについてある程度詳しくなって、そこから次のステップに進むのが良いのかな、と個人的に感じました。

それでは、今回はこの辺で。