メインコンテンツへスキップ
友田 陽大
AWS Lambda 本番運用
AWS
Lambda
テスト
サーバーレス
テスト容易性

Lambdaのテスト戦略:単体・結合・E2Eの設計、SDKモック、sam local、クラウドでの検証

AWS Lambdaを本番品質でテストする実装ガイド。AWS公式が定義する単体/結合/E2Eと『クラウドでのテストを優先せよ』という指針、薄いハンドラと純粋ロジックの単体テスト、aws-sdk-client-mock/motoによるSDKモック、sam localの使いどころと限界、使い捨てスタックによる結合・非同期の副作用検証まで、AWS公式仕様に忠実な実コードで解説します。

公開日
読了時間
12分
著者
友田 陽大
シェア

「Lambda のテスト、ローカルでは全部通るのに、本番にデプロイすると権限エラーやイベント形状の違いで落ちる」——サーバーレスのテストで最も多い失望です。原因は明確で、Lambda のロジックの多くは『コード』ではなく『クラウドのサービス設定(IAM・イベントソース・クォータ)』に宿るから。手元のモックは、その肝心な部分を再現できません。

この記事は、AWS Lambda を本番品質でテストするための戦略ガイドです。AWS公式のテスト指針を起点に、薄いハンドラの単体テストSDKのモックsam local の使いどころと限界クラウドでの結合/E2Eまでを一気通貫で解説します。題材として、私が中核開発者として構築したサーバーレス決済プラットフォーム本番二重課金0件)を支えたテスト規律も交えます。Lambda 本体の実行モデルは姉妹記事 AWS Lambda 本番運用ガイド に委ね、本稿は**「どうテストするか」一点**に集中します。

この記事のルール:テストの定義・指針・ツール名は AWS 公式ドキュメント(Lambda testing guide ほか・2026年6月時点) と各ツールの公式ページに基づきます。ツールのバージョン・APIは改定されるため、本番導入前に必ず公式(末尾「参考」)で確認してください。


0. メンタルモデル:「クラウドでのテスト」を最優先にする

まず、AWS公式の最重要メッセージを受け取ります。これがサーバーレステストの設計を決めます。

  • 公式の指針:クラウドでのテストを最優先せよ。公式は明言します——「クラウドベースのテストが、関数とアプリケーションの品質を最も正確に測る」「セキュリティポリシー・サービス設定・クォータ・最新のAPIシグネチャまで包括的にテストできるのはクラウドだけ」。
  • モックの限界:「モックを使うテストは机上では通ってもクラウドで失敗しうる。結果が最新APIと一致しないことがあり、サービス設定やクォータはテストできない」。
  • エミュレータの限界:「エミュレータに依存するテストはローカルで成功してもクラウドで失敗しうる(本番のセキュリティポリシー・サービス間設定・クォータ超過のため)」「エミュレータは控えめに使え」。
  • だから比重が変わる:普通のアプリより結合テストの比重を上げる。なぜなら、ロジックの多くがサービス設定(IAM・イベントマッピング)に宿るから。「マネージドサービスそのものはテスト不要だが、それらとの結合はテストが必要」。

公式が定義する3層を、この順で設計します。

公式の定義
単体(unit)隔離されたコードブロックに対するテスト配送料計算のビジネスロジック検証
結合(integration)2つ以上のコンポーネント/サービスの相互作用(通常クラウド)関数がキューのイベントを処理することの検証
E2Eアプリ全体の振る舞いイベントが各サービス間を流れて注文が記録される

1. 単体テスト:薄いハンドラ+純粋ロジック

公式のテスト容易性の指針は一言です——「ハンドラはイベントを受け取り、ビジネスロジックに必要な詳細だけを渡す薄いアダプタにせよ」。こうすれば、Lambda 固有の事情を気にせず、ビジネスロジックを普通の単体テストで固められます。

// domain.ts — Lambdaを一切importしない純粋ロジック。最速・最安で大量のケースを回せる
export interface Order { id: string; amount: number; destination: "domestic" | "international"; }

export function deliveryFee(order: Order): number {
  if (order.amount < 0) throw new RangeError("amount must be non-negative"); // 境界で検証
  const base = order.destination === "international" ? 2000 : 500;
  return order.amount >= 10_000 ? 0 : base; // 1万円以上は送料無料
}
// handler.ts — 薄いアダプタ。eventからの抽出・検証だけを担い、判断はdomainに委ねる
import type { APIGatewayProxyEventV2 } from "aws-lambda";
import { deliveryFee, type Order } from "./domain";

export const handler = async (event: APIGatewayProxyEventV2) => {
  const body = JSON.parse(event.body ?? "{}") as Partial<Order>;
  if (!body.id || typeof body.amount !== "number" || !body.destination) {
    return { statusCode: 422, body: JSON.stringify({ message: "invalid order" }) };
  }
  return { statusCode: 200, body: JSON.stringify({ fee: deliveryFee(body as Order) }) };
};
// domain.test.ts — 純粋関数なのでクラウドもDockerも不要。境界値を厚くテストする
import { describe, it, expect } from "vitest";
import { deliveryFee } from "./domain";

describe("deliveryFee", () => {
  it("国内は500円、国際は2000円", () => {
    expect(deliveryFee({ id: "a", amount: 100, destination: "domestic" })).toBe(500);
    expect(deliveryFee({ id: "b", amount: 100, destination: "international" })).toBe(2000);
  });
  it("1万円以上は送料無料", () => {
    expect(deliveryFee({ id: "c", amount: 10_000, destination: "international" })).toBe(0);
  });
  it("負の金額は拒否する", () => {
    expect(() => deliveryFee({ id: "d", amount: -1, destination: "domestic" })).toThrow(RangeError);
  });
});

現実的なイベントで叩く:ハンドラ自体を単体テストするなら、sam local generate-event本物に近いサンプルイベント(API Gateway/S3/SQS等)を生成してくれます。手書きのイベントは形状がズレやすいので、生成物を基にします。


2. SDKのモック:依存を切って単体テストを速く保つ

ビジネスロジックの外側で、DynamoDB等のSDK呼び出しを含む薄い層を単体テストしたいことがあります。ここはSDKをモックします。本物のAWSに繋がないので速く、決定的です。

2.1 TypeScript:aws-sdk-client-mock

AWS SDK for JavaScript v3 のクライアントをモックする標準ライブラリが aws-sdk-client-mockAWS SDK for JavaScriptチーム推奨)。mockClient() で包み、.on(Command).resolves(...) で応答を定義します。

// repo.test.ts — DynamoDBDocumentClient をモックし、本物のAWSに繋がず単体テスト
import { mockClient } from "aws-sdk-client-mock";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { describe, it, expect, beforeEach } from "vitest";
import { getOrder } from "./repo";

const ddbMock = mockClient(DynamoDBDocumentClient);
beforeEach(() => ddbMock.reset());

it("注文が見つかればドメインオブジェクトに変換して返す", async () => {
  ddbMock.on(GetCommand).resolves({ Item: { id: "o1", amount: 1200 } });
  await expect(getOrder("o1")).resolves.toEqual({ id: "o1", amount: 1200 });
});

it("見つからなければ null", async () => {
  ddbMock.on(GetCommand).resolves({ Item: undefined });
  await expect(getOrder("missing")).resolves.toBeNull();
});

2.2 Python:moto

Python では moto(公式の testing guide が名指しで紹介)。@mock_aws デコレータで boto3 呼び出しをインメモリにモックします。

# test_repo.py — moto で DynamoDB をモック(実リソース不要・インメモリ)
import boto3
from moto import mock_aws
from repo import get_order

@mock_aws
def test_get_order_returns_none_when_missing():
    ddb = boto3.resource("dynamodb", region_name="ap-northeast-1")
    ddb.create_table(
        TableName="orders",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
        BillingMode="PAY_PER_REQUEST",
    )
    assert get_order("missing") is None

Powertoolsのテスト:構造化ログ/トレース/メトリクスを使う関数のテストでは、POWERTOOLS_DEV を立てるとLoggerは整形出力、Tracerは無効化、Metricsは標準出力への送出を無効化します。POWERTOOLS_SERVICE_NAME / POWERTOOLS_METRICS_NAMESPACE をテスト前に設定しておくと、メトリクスのスキーマ検証で落ちるのを防げます。

モックの注意:公式が言う通り、モックはIAM権限・クォータ・サービス設定を検証できない。単体テストは「ロジックの正しさ」専用と割り切り、設定の正しさはクラウドの結合テストで担保します(次章)。


3. ローカル検証:sam local の使いどころと限界

手元で素早く動かしたいときは AWS SAM のローカルコマンド。3つを役割で押さえます(いずれもDocker前提)。

コマンド何をするか既定ポート
sam local invoke関数を1回ローカル実行--event でイベント注入)
sam local start-apiローカルHTTPサーバでAPIとして叩く3000
sam local start-lambdaLambdaサービスを模したローカルエンドポイント(CLI/SDKから呼ぶ)3001
# 生成イベントでローカル1回実行 → 入出力を素早く確認
sam local generate-event apigateway http-api-proxy > events/http.json
sam local invoke OrdersFunction --event events/http.json

限界を正しく理解するのが本章の主眼です。公式は明言します——「ローカルテストは迅速な開発・デプロイ前確認には良いが、クラウド上のリソース間の権限などは検証できない可能な限りクラウドでテストせよ」。sam local で叩いて関数からAWSを呼ぶと、呼び出しIDがLambdaサービス由来ではなくなり、実行ロールを引き受けない——つまりIAMの検証にならない。だから sam local は「ロジックの素早い確認」に留め、本番品質はクラウドで担保します。


4. 結合・E2E:クラウドで、使い捨てスタックで

サーバーレスの品質は、結局クラウドで本物のサービスに対してテストしないと担保できません。公式の推奨パターンを実装に落とします。

4.1 ブランチ/PRごとに使い捨てスタックを立てる

公式は「ユニークな名前のリソースを各スタックに作る」「コードのブランチ境界でスタックを分ける」ことをベストプラクティスとします。CIでPRごとに一意プレフィックスのスタックをデプロイ→テスト→破棄します。

# 結合テストCI(抜粋):PRごとに使い捨てスタックを立て、本物のサービスでテストし、必ず壊す
jobs:
  integration:
    runs-on: ubuntu-latest
    permissions: { id-token: write, contents: read } # OIDCで鍵レス(デプロイ記事参照)
    env: { STACK: orders-pr-${{ github.event.number }} }
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v6
        with: { role-to-assume: arn:aws:iam::123456789012:role/ci-deploy, aws-region: ap-northeast-1 }
      - uses: aws-actions/setup-sam@v2
      - run: sam build && sam deploy --stack-name "$STACK" --no-confirm-changeset --resolve-s3
      - run: npm run test:integration          # 本物のAPI/DB/キューに対して検証
      - if: always()                            # 成否に関わらずスタックを破棄(コスト/汚染防止)
        run: sam delete --stack-name "$STACK" --no-prompts

4.2 非同期は「副作用」をポーリングで検証する

イベント駆動(SQS/EventBridge/Streams)は戻り値が無いので、下流の副作用を観測してアサートします。公式のArrange-Act-Assert:関数を起動(Act)→宛先(SQS/DynamoDB)から結果を取得して検証(Assert)

// 非同期フローの結合テスト:イベント投入 → 下流の副作用が現れるまでポーリングして検証
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";

async function waitForItem(table: string, id: string, timeoutMs = 30_000) {
  const ddb = new DynamoDBClient({});
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { Item } = await ddb.send(new GetItemCommand({ TableName: table, Key: { id: { S: id } } }));
    if (Item) return Item;                        // 副作用が現れた=関数が正しく処理した
    await new Promise((r) => setTimeout(r, 1000)); // 1秒間隔でポーリング
  }
  throw new Error(`item ${id} did not appear within ${timeoutMs}ms`); // 失敗を明確に
}

E2Eはモックを使わない(公式)。API Gateway→Lambda→DynamoDB を通しで叩き、各サービスの設定とIAM権限まで含めて検証します。これが「ローカルで通って本番で落ちる」を構造的に潰す唯一の方法です。


5. サーバーレスのテストピラミッド:比重を組み替える

普通のアプリの「単体多め・E2E少なめ」を、サーバーレスでは結合に厚く組み替えます(公式が「結合テストに注力せよ」と明言)。

        ▲  E2E(少数・モック禁止)
       ╱ ╲   主要なユーザーフローを通しで。設定・IAM・サービス間連携を検証
      ╱   ╲
     ╱ 結合 ╲  ← サーバーレスはここが厚い
    ╱  多め  ╲   関数×実サービス(DB/キュー/API GW)。設定の正しさはここでしか分からない
   ╱─────────╲
  ╱   単体     ╲  最多・最速・最安。純粋ロジック+SDKモック。IAMやクォータは検証しない
 ╱─────────────╲

設計指針:

  • 単体:ビジネスロジック(純粋関数)と、SDKモックでの薄いデータ層。速く・大量に・境界値を厚く
  • 結合:関数を実サービスに繋いで、設定・権限・イベント形状を検証。サーバーレスではここが品質の主戦場
  • E2E:主要フローを通しで、モック禁止。数は絞る。
  • ローカル(sam local):ピラミッドの「層」ではなく、開発中の素早いフィードバック手段。品質保証の代替にはしない。

6. まとめ:Lambdaテスト戦略チートシート

  • 最優先はクラウドでのテスト(公式)。モック/エミュレータはIAM・クォータ・設定・最新API差分を再現できない。「机上で通って本番で落ちる」の正体。
  • 単体:ハンドラは薄いアダプタ、ビジネスロジックは純粋関数に切り出して厚くテスト。sam local generate-event で現実的なイベントを使う。
  • SDKモック:TypeScript=aws-sdk-client-mock(AWS SDK for JSチーム推奨)、Python=moto@mock_aws・公式紹介)。ロジック専用、設定は検証しない。
  • sam localinvoke/start-api(3000)/start-lambda(3001)はDocker前提。権限は検証できないので控えめに。
  • 結合/E2Eブランチ/PRごとの使い捨てスタックを立て→実サービスで検証→必ず破棄。非同期は副作用をポーリング。E2Eはモック禁止。
  • ピラミッド:サーバーレスは結合に厚く。設定とIAMはそこでしか守れない。

私は決済プラットフォームで、本番二重課金0件を「祈り」ではなくテストで担保しました。冪等化ロジックを純粋関数で厚く単体テストし、SQS→Lambda→DynamoDBの冪等な副作用を使い捨てスタックの結合テストで検証し、E2Eで決済フローを通しで守る——この層構造が、止められない決済基盤を安心して変更し続ける土台です。テストを含むAI駆動開発の品質ゲート設計は 品質ゲートの作り方 も参照ください。

「自社のサーバーレスを、机上ではなく本番で確実に動く形でテストしたい」——テストピラミッドの設計から使い捨てスタックのCI、非同期の副作用検証まで、一人 × 生成AI(Claude Code)の速さで伴走します。 現状のテスト戦略の診断からでも、お気軽にご相談ください。


参考(公式ドキュメント)

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

環境分野のサーバーレス決済プラットフォーム(フルスタック開発・決済信頼性レイヤーを主導)

ケーススタディを見る