🌟

Lambda Pythonでmotoを使ったテスト手法

に公開

1. 結論(この記事で得られること)

この記事を読むと、以下が身につきます。

  • motoライブラリを使ったLambda関数の単体テストの基本と実践パターン
  • AWSサービス(S3/DynamoDB/SQS等)をモックする正しい設計
  • テストコードがメンテナブルになる構造化テクニック
  • AI(Claude/GPT)を使ってmotoのエラーを10分で解決する方法
  • 本番デプロイ前にCI/CDで確実に検証するテスト戦略

私自身、過去にmotoを使わず「実際のAWSリソースに接続するテスト」を書いていた時期がありました。結果、テスト実行に数分かかる・並列実行できない・CI環境でクレデンシャル管理が煩雑になる、という三重苦に陥りました。

motoを正しく導入してからは、テストが数秒で完了し、開発速度が劇的に向上しました。

2. 前提(環境・読者層)

対象読者

  • LambdaでPythonを使っているエンジニア
  • AWSサービス連携のテストに苦労している方
  • 単体テストと結合テストの境界線が曖昧で困っている方

前提環境

  • Python 3.9 以上(Lambda実行環境に準拠)
  • pytest 7.x 以上
  • moto 4.x 以上
  • boto3(AWS SDK for Python)

必要な知識

  • Lambdaの基本的な実装経験
  • pytestの基礎(fixture, parametrizeなど)
  • boto3でのAWS操作経験

3. Before:よくあるつまずきポイント

3-1. 実AWSリソースに接続するテストの問題

# ❌ 危険なパターン:本物のS3に接続している
import boto3
 
def test_process_s3_file():
    s3 = boto3.client('s3')
    # テスト用バケットに依存...
    s3.put_object(Bucket='test-bucket', Key='test.txt', Body=b'data')
    result = my_lambda_handler({'bucket': 'test-bucket', 'key': 'test.txt'}, {})
    assert result['statusCode'] == 200

問題点

  • テスト実行に時間がかかる(ネットワークI/O)
  • 並列実行でリソースが競合する
  • AWSクレデンシャルの管理が煩雑
  • CI環境で実AWSアカウントが必要になる
  • 課金が発生する可能性

3-2. モック設計が曖昧で壊れやすい

# ❌ よくある失敗:部分的にモックして混乱
from unittest.mock import patch
 
@patch('boto3.client')
def test_lambda(mock_client):
    # どのメソッドをモックするか統一ルールがない
    mock_s3 = MagicMock()
    mock_client.return_value = mock_s3
    # テストが複雑化...

昔の私もこのパターンで書いていて、「このテスト、何をテストしてるんだっけ?」と後から見返して混乱した経験があります。

3-3. 環境変数やLambdaコンテキストの扱い

# ❌ 環境変数がハードコードされている
def lambda_handler(event, context):
    bucket = 'production-bucket'  # 環境変数で制御すべき
    s3 = boto3.client('s3')
    ...

テスト時に環境変数を切り替えられず、プロダクションコードとテストコードの境界が曖昧になります。

4. After:基本的な解決パターン

4-1. motoを使った基本構成

# lambda_function.py
import boto3
import os
 
def lambda_handler(event, context):
    bucket_name = os.environ.get('BUCKET_NAME')
    s3 = boto3.client('s3')
 
    key = event.get('key')
    obj = s3.get_object(Bucket=bucket_name, Key=key)
    body = obj['Body'].read().decode('utf-8')
 
    return {
        'statusCode': 200,
        'body': f'Processed: {body}'
    }
# test_lambda_function.py
import pytest
import os
from moto import mock_s3
import boto3
from lambda_function import lambda_handler
 
@pytest.fixture
def aws_credentials():
    """モック用の認証情報を設定"""
    os.environ['AWS_ACCESS_KEY_ID'] = 'testing'
    os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'
    os.environ['AWS_SECURITY_TOKEN'] = 'testing'
    os.environ['AWS_SESSION_TOKEN'] = 'testing'
    os.environ['AWS_DEFAULT_REGION'] = 'ap-northeast-1'
 
@pytest.fixture
def s3_client(aws_credentials):
    """モックS3クライアントを返す"""
    with mock_s3():
        yield boto3.client('s3', region_name='ap-northeast-1')
 
@pytest.fixture
def setup_s3_bucket(s3_client):
    """テスト用バケットを作成"""
    bucket_name = 'test-bucket'
    s3_client.create_bucket(
        Bucket=bucket_name,
        CreateBucketConfiguration={'LocationConstraint': 'ap-northeast-1'}
    )
    return bucket_name
 
def test_lambda_handler_success(s3_client, setup_s3_bucket):
    """正常系:S3からファイルを取得して処理"""
    # Arrange
    os.environ['BUCKET_NAME'] = setup_s3_bucket
    test_key = 'test.txt'
    test_body = 'Hello Lambda'
    s3_client.put_object(
        Bucket=setup_s3_bucket,
        Key=test_key,
        Body=test_body.encode('utf-8')
    )
 
    event = {'key': test_key}
    context = {}
 
    # Act
    with mock_s3():
        # motoのコンテキスト内でLambda実行
        response = lambda_handler(event, context)
 
    # Assert
    assert response['statusCode'] == 200
    assert 'Processed: Hello Lambda' in response['body']

ポイント

  • 「@mock_s3()」デコレータでS3の振る舞いを完全にモック化
  • fixtureで事前準備を構造化(再利用可能)
  • 環境変数は「os.environ」で注入(テストごとに切り替え可能)

続きはnoteで

この記事の実装編・詳細解説はnoteで公開しています。

実際のコード例や、実務で遭遇するハマりポイントなど、より踏み込んだ内容を書いています。

Discussion