AWS Lambda のテスト方法を紹介!スモーク・ユニット・統合テストで堅牢安全な Lambda 関数実装を実現しましょう😎

AWS Lambda のテスト方法を紹介!スモーク・ユニット・統合テストで堅牢安全な Lambda 関数実装を実現しましょう😎

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!

読者のみなさんも、AWS でのアプリケーション開発で Lambda 関数をよく用いるのではないでしょうか?

本記事では、堅牢安全な Lambda 関数を実現するための AWS Lambda のテスト方法を3つ紹介します!今回は JavaScript にフォーカスしています。

想定する読者

  • AWS Lambda を使っているヒト
  • AWS Lambda のテスト方法を理解したいヒト

はじめに

AWS Lambdaのメリットとデメリット

ソフトウェア開発にとって、AWS Lambda のような FaaS(Function as a Service)オファリングはありがたいものです。

  • バックエンドのインフラのセットアップとメンテナンスに伴う問題の多くを解決し工数削減できる
  • 機能同士を疎結合にし、デグレードを防止できる
  • マイクロサービスアーキテクチャー化により、機能のモジュール化とカプセル化を促す

だだしAWS Lambdaにはメリットと引き換えに、テストのベストプラクティスが確立されていないため、ビジネスロジックのテストを行うのがたびたび難しいことがあります。PHPでいうLaravel、JavaだとPlayFrameworkのようにフレームワークから提供されているベストプラクティスから確立されたテストモジュールが存在しません。JestAWSMockなどのテスト時の最低限のモジュールは存在するので、それらを組み合わせて実現する形になります。

そのため、AWS Lambdaのユニットテストは、多くのプロジェクト・エンジニアで意見が対立してしまいます。例えば、高速なイテレーションが可能にするために、自分のコードを中心にすべてをエミュレートするローカル接続(LocalStackServerlessOffline等々)のアプローチを好む人がいます。一方で、それらのエミュレート環境でのテストに対し、実際に動作する環境でテストしているわけではないので、誤った安心感を与えてしまうと考える人もいるのです。LocalStackは話題ですが、実際の挙動と異なる箇所が多く、まだ成熟には至っていません。

AWS Lambdaのテストの焦点

では、何をテストすべきなのでしょうか?

もちろん、エンジニアが実装したコードをテストするのですが、FaaS が真価を発揮するアーキテクチャのメインは、様々なサービスとの統合にあります。

AWS Lambda は、AWS やサードパーティが提供するすべてのマネージドサービスをつなぐ万能なHUBとも言えます。そのため、テストの主な焦点は、自分のコードだけではなく、それがどのように別のサービスと統合されるかということです。なぜなら、イベントを読み込んで書き出すだけの Lambda は珍しく、通常は、S3・Step Functions・RDS のようなひとつか複数の他のサービスにアクセスすることになるためです。

AWS Lambdaのテストでは、ビジネスロジックが勿論のこと、正しく他サービスと接続できているか、その接続方法は意図したものなのかを、検証するよう心がけましょう。

基本的な3つのテスト方法

今回紹介するテスト方法は、以下の3つです。

  • スモークテスト
  • ユニットテスト(単体テスト)
  • 統合テスト

早速、それぞれ詳しく見ていきましょう。

1. スモークテスト

スモークテストはシンプルなテストで、コードを実行しようとしたときに、クラッシュしないことだけを確認します。つまり、コードが正しく動作するかどうかを確認しないのです。そのため、テストでは実行されない if 分岐のどこかにバグがある可能性もあります。同じく、ロジックの問題についてもテストしません。

ウェブサーバーの場合、スモークテストはサーバーの起動を指します。サーバーにリクエストを送らず、ただサーバーを起動してクラッシュするかどうかを確認します。これは簡単にできますし、もし失敗しても他のテストの実行時間を短縮できます。

また Lambda はイベントを処理したときだけ実行され、直後にフリーズかリタイアされるため、イベントの起動と処理のアクションは同じです。

つまり、スモークテストは Lambda 関数にイベントを送り、それがエラーを投げるかどうかを確認するものということです。Lambda 関数が処理できると思われる一番シンプルなものを使っても構いません。

そして、スモークテストは AWS CLI から、以下のコマンドで行うことができます。

$ aws lambda invoke \
--cli-binary-format raw-in-base64-out \
--function-name <LAMBDA_FUNCTION_NAME> \
--payload --payload file://<JSON_EVENT_FILE>

自動化のためには、このような CLI コマンドを AWS SDKまたはbash スクリプトで作成し、他のテストの実行前に実行します。

2. ユニットテスト(単体テスト)

ユニットテストは、関数のロジックを実際にテストするので、スモークテストよりも少し複雑です。ほとんどのエラーは、他のサービスとコードを統合するときに発生するので、統合テストと比べてそれほど大きなメリットはありません。

では、ユニットテストを始めていきましょう。まずは、テストしたいロジックを JavaScript (分かりやすくTypeScriptではなくJavaScriptで記載)で作成します。次の例では、演算の引数に応じて2つの数値を足し引きする Lambda 関数を見てみましょう。

exports.handler = async (event) => {
  if (event.queryStringParameters.operation === "substract")
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body:
        event.queryStringParameters.x - 
        event.queryStringParameters.y
    }

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body:
      event.queryStringParameters.x + 
      event.queryStringParameters.y
  }
}

これは不自然な例ですが、それでもこの関数は必要以上にテストしにくいものです。queryStringParameters フィールドを含むイベントオブジェクトを作成する必要があり、そこには operationxy フィールドが存在しなければなりません。

もし、このロジックを3つの引数しか必要としないプレーンな JavaScript 関数にカプセル化すれば、さらにシンプルになります。

const addOrSubtract = (operation, x, y) => 
  operation === "substract" ? x - y : x + y;

exports.handler = async (event) => {
  const { operation, x, y } = event.queryStringParameters;

  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: addOrSubtract(operation, x, y)
  };
};

このリファクタリングされた例では、Lambda のハンドラから独立してロジックをテストできるようになります。

ハンドラー内に大量のロジックを作成するのではなく、上記のように例のようにリファクタリングすることを心がけましょう。テストしやすいコードは良いコードと言えます。

3. 統合テスト(結合テスト)

統合テストは、FaaS のテストで最も重要です。先述の通り、AWS Lambda はマネージドクラウドサービスをつなぐために使われることがほとんどで、最も重要なテスト対象は、Lambda 関数が他のサービスと相互作用する部分です。

さて、統合テストには大きく分けて2つの方法があります。

  • 実際のインフラを使ったテスト
  • そのインフラをエミュレートしたものを使ったテスト

この2つのどちらにも長所と短所があります。例えば、モックアップのインフラを使ったテストは高速で安価ですが、モックが間違っていれば、テストも間違っていることになります。また、本物のインフラを使ったテストは、より信頼できる一方でコストがかかりますし、テストを実行するたびにインフラを用意する必要がある場合は、相応に時間がかかるでしょう。

また、統合テストを書くのに「タダ飯」はありません。実際のインフラに手を出さなくて済む分、モックアップのインフラを常に最新にするのに時間がかかってしまいます。

実際のインフラを使ったテスト方法

実際のインフラを使ったテストは、Infrastructure as Code (IaC) ツールを使っているときのみ意味があります。そうでなければ、リソースを手動でプロビジョニングするのに無駄に時間がかかってしまうのです。特にサーバーレスアプリケーションは、たくさんの小さなサービスを内包しがちです。

AWS は複数の IaC ツールを提供しています。AWSのエコシステムと非常によく統合されているものには、ServerlessCloudFormationSAMCDKなどが挙げられます。

選択したツールの準備ができたら、それを使ってひとつの IaC 定義でテストと本番にデプロイできます。このように、テスト環境と本番環境の一致を確認できるのです。

さて、テストでは Lambda 関数の入出力を確認します。

例えば、API-Gateway で発生する Lambda の同期呼び出しは、Lambda 関数に入るイベントと関数が返すレスポンスを指します。非同期呼び出しでは、値は返されません。

このようなテストで注目したいのが、関数が他のサービスにアクセスする方法です。関数が認証のために DynamoDB から何らかのデータを読み取る場合、該当データがアクセス可能で正しいことを確認してからテストを実行する必要があります。S3 に書き込む場合は、テスト実行後に S3 にアクセスし、すべてが正しく行われたかどうかを確認しなければなりません。

テスト内では、同じ AWS SDK for JavaScript を使用して、これらのサービスを確認できます。AWS Lambda 上でもテストを実行する場合は、プリインストールまでされます。

では、そんな統合テストがどのようなものかを、簡易的な例で見てみましょう。

const aws = require("aws-sdk");

const dynamoDb = new aws.DynamoDB.DocumentClient();
const lambda = new aws.Lambda();
const s3 = new aws.S3();

exports.handler = async (vent) => {
  await firstTest();
};

async function firstTest() {
  await dynamoDb
    .put({
      TableName: "Users",
      Item: {
        HashKey: "userId",
        isAdmin: true
      }
    })
    .promise();

  await lambda
    .invoke({
      FunctionName: "createAdminFile",
      Payload: JSON.stringify({
  userId: "userId",
      filename: "sample.txt",
  content: "OK" 
      })
    })
    .promise();

  const s3Object = await s3
    .getObject({
      Bucket: "admin-files",
      Key: "sample.txt"
    })
    .promise();

  checkFile(s3Object).contains("OK");

  await dynamoDb
    .delete({
      TableName: "Users",
      Key: { HashKey: "userId" }
    })
    .promise();

  await s3
    .deleteObject({
      Bucket: "admin-files",
      Key: "sample.txt"
    })
    .promise();
}

この例は、別の Lambda 関数をテストする Lambda 関数です。DynamoDB テーブルに admin 権限を持つユーザードキュメントを作成します。そして、event 引数で Lambda 関数を呼び出し、関数が呼び出された後にS3 内にファイルが作成されたことを確認し、テスト関連のデータをすべてクリーンアップするという流れです。

これは、便利さのために tape のようなテストフレームワークを含めた基本的な実装に過ぎませんが、シンプルな統合テストが機能するために必要なことが分かります。

アプリケーションをテストし、再テストすることはいくらでもできます。しかし、その製品が発売されて何かしらの問題が起こるのは世の常です。そんなときには、Dashbird の機能ビューを使って、アプリケーションがどのように動作しているかを正確に見ることができますし、アプリがおかしくなったときには、インシデント管理プラットフォームを使って、どこで何が壊れたかを正確に見ることができます。

https://dashbird.io/blog/test-javascript-lambda-functions/

まとめ

今回は、堅牢安全な Lambda 関数を実現するための AWS Lambda のテスト方法を紹介しました。

紹介したもの以外にも、このようにさまざまな種類のテストがあります。

  • E2E テストの様により大きな範囲を持つテスト
  • パフォーマンステストの様に関数の特定の振る舞いを確認するテスト

種類が多く、どれから手をつければいいか悩んでいる方は、まずはスモークテストと統合テストから始めるのがおすすめです。つまり、Lambda が呼び出し開始直後にクラッシュしないことを確認し、実際に他のサービスを正確に使用していることをテストしましょう。

また、複数のサービスを統合するためだけではなく、特定のロジック向けのとても複雑な Lambda 関数がある場合、そのロジックをカプセル化してユニットテストを実行してみましょう。そうすることで、より速く、より安くイテレーションを行うことができます。

ぜひ、本記事を参考にしながら AWS Lambda のテストをしてみてはいかがでしょうか。

サーバーレスに関する開発相談は、お気軽にお問い合わせください。