[Python] motoでLambdaやDynamoDBをモックしてunittestを実行
目的
- Pythonで書いたLambdaのAPIを標準ライブラリのunittestを使ってテスト。
- motoを使ってLambdaとDynamoDBをモックすることで、AWSに接続せずにunittestを動かす。
motoとは?
LambdaやDynamoDBに限らず、S3など様々なAWSサービスをモック出来る優れものです。
ただし、全てのAWSサービスに対応しているわけではないで、motoのドキュメントから対応サービスを確認してください。
ディレクトリ階層
root ├ apis │ └ handler.py ← Lambdaのソースコード └ test └ test_handler.py ← handler.pyに対するテストコード
ライブラリのインストール
$ pip install boto3 $ pip install docker $ pip install moto
各バージョン
- Python 3.8.5
- boto3 1.20.7
- docker 5.0.3
- moto 2.2.15
Lambdaの実装(handler.py)
リクエストパラメータのnameをDynamoDBのtest-tableという名前のテーブルに登録するだけのAPI。
import boto3 import json import uuid from http import HTTPStatus DYNAMO_DB = boto3.resource('dynamodb', region_name='ap-northeast-1') TEST_TABLE = DYNAMO_DB.Table('test-table') def handler(event, context): # リクエストパラメータ取得 body = json.loads(event.get('body') or '{}') # DynamoDBに登録 item = { 'id': str(uuid.uuid4()), 'name': body.get('name') } TEST_TABLE.put_item(Item=item) # 処理結果返却 return { 'statusCode': HTTPStatus.OK, 'body': json.dumps({'message': 'DB登録完了'}) }
テストコードの実装(test_handler.py)
import boto3 import json import unittest from apis import handler from http import HTTPStatus from moto import mock_lambda, mock_dynamodb2 # テスト対象のテーブル名 TABLE_NAME = 'test-table' # Lambdaのeventを生成 def create_event(body): return { 'body': json.dumps(body), 'requestContext': {} } class TestHandler(unittest.TestCase): # テスト実行前の処理 def setUp(self): pass # テスト実行後の処理 def tearDown(self): pass # motoにテーブルを作成 def create_table(self): db = boto3.resource('dynamodb', region_name='ap-northeast-1') db.create_table( TableName=TABLE_NAME, KeySchema=[ { 'KeyType': 'HASH', 'AttributeName': 'id' }, { 'KeyType': 'RANGE', 'AttributeName': 'name' } ], AttributeDefinitions=[ { 'AttributeName': 'id', 'AttributeType': 'S' }, { 'AttributeName': 'name', 'AttributeType': 'S' } ], BillingMode='PAY_PER_REQUEST' ) table = db.Table(TABLE_NAME) handler.TEST_TABLE = table return table # テスト実行 @mock_dynamodb2 @mock_lambda def test_handler(self): # テーブル作成 table = self.create_table() # リクエストパラメータ name = 'moto-test' request = { 'name': name, } # Lambda実行 response = handler.handler(create_event(request), {}) body = json.loads(response['body']) # HTTPステータスチェック assert HTTPStatus.OK == response['statusCode'] # レスポンスメッセージチェック assert body['message'] == 'DB登録完了' # DB登録結果確認 data = table.scan()['Items'] assert len(data) == 1 assert data[0]['name'] == name
テスト実施
$ python -m unittest discover test/ . ---------------------------------------------------------------------- Ran 1 test in 0.192s OK
解説
moto から Lambda と DynamoDB のモックするデコレータをimportする。
from moto import mock_lambda, mock_dynamodb2
テスト関数に Lambda と DynamoDB のモックするデコレータを設定。
このデコレータを設定した関数から呼び出される Lambda と DynamoDB はAWSに接続せず、motoのモックを参照します。
# テスト実行 @mock_dynamodb2 @mock_lambda def test_handler(self): # テーブル作成 table = self.create_table()
motoにテーブルを作成する。
AWS上のDynamoDBにテーブルがあったとしても、モックしているmotoになければエラーになるので、実際と同じ定義のテーブルを作成します。
def create_table(self): db = boto3.resource('dynamodb', region_name='ap-northeast-1') db.create_table( TableName=TABLE_NAME, KeySchema=[ { 'KeyType': 'HASH', 'AttributeName': 'id' }, { 'KeyType': 'RANGE', 'AttributeName': 'name' } ], AttributeDefinitions=[ { 'AttributeName': 'id', 'AttributeType': 'S' }, { 'AttributeName': 'name', 'AttributeType': 'S' } ], BillingMode='PAY_PER_REQUEST' ) table = db.Table(TABLE_NAME) handler.TEST_TABLE = table return table
注意点
モックの方法を間違えるとAWSに繋ぎに行ってしまうため、既存リソースに影響を与えてしまう可能性があります。
なのでmotoを使ってテスト方法を試行錯誤する場合は、こまめにAWSのコンソール画面を確認しに行って、テストデータが作られていないか確認することをオススメします。
まぁ、そもそもローカルからAWSに繋がるような設定をしないのが1番安全ですがね。