AWS Lambda - Test のアンチパターン

# はじめに

Lambda 関数 のテストについていろいろと試行錯誤した結果、見通しが悪かった方法をアンチパターンとして共有します。
「MySQL アンチパターン」みたいなノリです。
FaaS として AWS Lambda を前提として記事を書いていますが、同じ考え方は他のFaaSにも適用できると思います。
専門的な概念を指す用語についてはなるべく注を書くようにしました。

# :poop: アンチパターン: AWS コンソール上のエディターを開発環境として使う

Lambda 関数 を使い始めた人がまず最初にとる方法です。お手軽に始められて便利ですが、本格的に開発する場合は辛いです。ちょっとした動作確認や、数10行程度の小規模なスクリプトの開発のみに使いましょう。

## 具体的な方法

* AWS コンソール上で直接 Lambda 関数 を編集する。手動で関数を実行し、目視で結果を確認する

## 見通しが悪い理由

* Lambda 関数 の起動してから結果を得るまでに数秒かかる
* 修正しながら同じテストを何度も再現するのがダルい。
    * ソースコード修正後に一度チェックをパスした試験項目がデグレる。。
* テストに失敗した場合にどこで失敗したのかがすぐにわからない
* 実行後に Typo や構文エラーがあることがわかり、再実行。。
* エディタが好きなものを使えない
* etc.

## 推奨する別の方法

* ローカルで作成したテスト済みのリソースを Lambda 関数 にデプロイする
    * デプロイツールとしては AWS SAM CLI, Serverless Flamework, APEX などがある [^deploy-tool]

[^deploy-tool]: デプロイツールの比較については[過去記事](https://github.com/billthelizard/aws-sam-localstack-example)も参照してみてください。

# :poop: アンチパターン: ローカルで Lambda 関数 を直接起動して実行結果を確認する

Lambda 関数 をローカルに起動すれば、テストの実行時間が短縮するように思えますが、テストが手動だと時間短縮はほとんど見込めません。また、E2Eテスト[^E2E]を実装しても、大きなテストしか書けず、失敗した原因の調査に時間がかかります。アプリケーションが複雑になるほど開発効率は悪化します。

## 具体的な方法

* `sam local invoke` を使って Docker コンテナ上に起動した Lambda 関数 を起動し、結果を目視で確認する、またはE2Eテストを実装する
* `lambci/docker-lambda` [^docker-lambda] イメージから起動した Docker コンテナ上に Lambda 関数 を起動し、結果を目視で確認する、またはE2Eテストを実装する

[^E2E]: End To End Test。システムの外側から見た振る舞いを検査するテスト。
[^docker-lambda]: Lambda 関数 と同一の環境を再現した Docker Image

## 見通しが悪い理由

* Docker コンテナの起動に数秒かかかる。
* 手動テストの場合、同じテストを何度も再現するのがダルい。
    * ソースコード修正後に一度チェックをパスした試験項目がデグレる。。
* E2Eテストを実装する場合、大きいテストしか書けないので、実行に時間がかかる。
* テストに失敗した場合にどこで失敗したのかがすぐにわからない

## 推奨する別の方法

* Lambda 関数 を直接起動するのでなく、Lambda 関数 の中身を別のコードにクラスや関数として切り出し、Lambda 関数 からそのコードを呼び出すようにする
* 別のコードに切り出した関数やクラスの単体テストを実装する

# :poop: アンチパターン: ユニットテスト実行時、AWSマネージドサービスに直接アクセスする

## 具体的な方法

* Lambda 関数 から機能を切り出し、単体テストを実装している
* ユニットテスト実行時、テストコードから AWS マネージドサービスに直接アクセスする

## 見通しが悪い理由

* 1回の接続で数秒かかります。何回も接続するとテスト実行から完了までに時間がかかります（数十秒〜数分）
* AWS マネージドサービスの利用コストがかかります

## 推奨する別の方法

* AWSマネージドサービスと同一の機能を再現した疑似サービス（localstack, DynamoDB local など）をローカルに起動し、テストコードから Mock Service に接続する
* AWSマネージドサービスのインターフェースのMock[^mock] を作成する[^mock-example]。または、他の人が作った Mock ライブラリを利用する[^third-party-mock-lib]。

[^mock-example]: この記事で紹介されているような方法です。https://qiita.com/horike37/items/034f731f1b40c5a28e74
[^third-party-mock-lib]: https://github.com/dwyl/aws-sdk-mock

[^mock]: あるサービスのインターフェースを擬似的に再現したクラスや関数のこと。

# :green_apple: ベストプラクティス

以上をまとめると、Lambda 関数 テストのベストプラクティスは以下のようになります。

* ローカルでテスト済みのリソースを Lambda 関数 にデプロイする
    * AWS SAM CLI, Serverless Framework, APEX などのデプロイツールを使う
* Lambda 関数 を直接起動するのでなく、Lambda 関数 の中身を別のコードにクラスや関数として切り出し、Lambda 関数 からそのコードを呼び出すようにする
* 別のコードに切り出した関数やクラスの単体テストを実装する
* AWSマネージドサービスと同一の機能を再現した疑似サービス（localstack, DynamoDB local など）をローカルに起動し、テストコードから Mock Service に接続する
* AWSマネージドサービスのインターフェースを模した Mock クラスや関数を作成する。または、他の人が作った Mock を利用する。

具体的なテストの実装方法やリソースの作り方についてはサンプルコードを作っていますので[こちら](https://github.com/billthelizard/aws-sam-localstack-example)を参照してください。

過去の記事「[Lambda 関数 の開発について悩んだところをまとめてみた](https://qiita.com/billthelizard/items/be87a7662c34fd5c634e)」にもLambda 関数 のテストについて記述がありますので、参考にしてみてください。

# 終わりに

そもそも、[和田卓人さんのスライド](https://speakerdeck.com/twada/testable-lambda-working-effectively-with-legacy-lambda)で自分が言いたいことはすべてコンパクトにまとまっていますが、高度な概念が出てきてとっつきにくい人もいるかも？ と思ったので、あえて泥臭い書き方で書きました。