boto3からDynamoDBへのアクセスをmotoでモックしてみる

スタートプラン

サーバーレス開発部@大阪の岩田です。 現在開発しているLambdaで単体テストを作成する際に、DynamoDBへのアクセスをモックしたい箇所が出てきました。 その際にテスト手法について色々と調査したことをまとめます。

背景

LambdaからDynamoDBにアクセスする処理のテストコードを書く場合、DynamoDBのエンドポイントとしてDynamoDB LocalもしくはLocalStackを使用して、ローカル環境でテストを実行する方法があります。

今回自分がやりたかったのはもう少し前段階のテストで、単純なキー指定の参照処理の単体テストでした。 もう少し開発が進んで、結合テストのフェーズになると、DynanoDB LocalやLocalStackが有用になると思うのですが、現時点では外部サービスに依存せずに、完全にPythonのコードのみでテストを完結させたかったので、DynamoDBへのアクセス部分を良い感じにモックできるライブラリを探していました。

motoというライブラリが要件に会いそうだったのですが、日本語の情報が少なく、ヒットする情報はS3をモックする方法ばかりだったので、motoのテストコードを見ながら手探りで実装してみました。

環境について

今回使用した環境は下記の通りです。

  • Python 3.6.5
  • pipenv 2018.05.18
  • SAM CLI 0.3.0
  • boto3 1.7.24
  • moto 1.3.3
  • pytest 3.5.1

前準備

まずはプロジェクトのひな形を作成します。

1
sam init --runtime python3.6

作成された雛形は下記のような構造になっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
└── sam-app
    ├── README.md
    ├── hello_world
    │   ├── __init__.py
    │   ├── __init__.pyc
    │   ├── app.py
    │   └── app.pyc
    ├── requirements.txt
    ├── template.yaml
    └── tests
        └── unit
            ├── __init__.py
            ├── __init__.pyc
            ├── test_handler.py
            └── test_handler.pyc

次に、必要なライブラリを導入します

1
2
3
4
cd sam-app
pipenv install --python 3.6.5
pipenv shell
pipenv install pytest moto --dev

以上で事前準備は完了です。

実装

URLでIDを受け取り、受け取ったIDをキーにDynamoDBを検索、検索結果を返却するようなAPIを実装します。 まずtemplate.yamlを下記のように修正します。 ※今回はmotoによる単体テストの調査が目的なので、デプロイ手法等については無視しています

1
2
3
4
5
6
7
# CodeUri: hello_world/build/ コメントアウト
#Handler: app.lambda_handler コメントアウト
Handler: hello_world/app.lambda_handler
#略
        Properties:
#         Path: /hello コメントアウト
          Path: /hello/{id}                       

次にpythonのソースコード(hello_world/app.py)を修正します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json
import boto3
 
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('hello')
 
def lambda_handler(event, context):
 
    id = event['pathParameters']['id']
    res = table.get_item(Key={'id': id})
 
    return {
        "statusCode": 200,
        "body": json.dumps(res)
    }

続いてテストコード(tests/unit/test_handler.py)を修正します。 まずは先ほどのSAMテンプレートの変更に合わせて、eventにidを渡すよう調整します。

1
2
3
4
5
6
7
8
9
10
11
def apigw_event():
    """ Generates API GW Event"""
 
    return {
        "body": "{ \"test\": \"body\"}",
        "resource": "/{proxy+}",
        "requestContext": {
#略
        "pathParameters": {
            "id": "1" #ココを修正
        },

次に、実際にテストコードを修正していきます。 まずインポート部分を修正します。

1
2
3
4
5
import json
import pytest
from hello_world import app
from moto import mock_dynamodb2
import boto3

テストコードの本体は下記のように修正しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@mock_dynamodb2
def test_lambda_handler(apigw_event):
 
    dynamodb = boto3.resource('dynamodb')
    dynamodb.create_table(
        TableName='hello',
        KeySchema=[
            {
                'AttributeName': 'id',
                'KeyType': 'HASH'
            }
        ],
        AttributeDefinitions=[
            {
                'AttributeName': 'id',
                'AttributeType': 'S'
            },
        ],
        ProvisionedThroughput={
            'ReadCapacityUnits': 10,
            'WriteCapacityUnits': 10
        }
    )
    table = dynamodb.Table('hello')
    item = {
        'id': '1',
        'message': 'hello',
    }
    table.put_item(Item=item)
 
    ret = app.lambda_handler(apigw_event, "")
    assert ret['statusCode'] == 200
 
    data = json.loads(ret['body'])
    # put_itemしたレコードがそのまま返却されることを確認する
    assert item == data['Item']

先頭に@mock_dynamodb2を記述することにより、motoがDynamoDBへのアクセスをモックしてくれます。 テストコードの中では、擬似的にテーブル作成とアイテムのputを行い、APIの戻り価が先ほどputしたアイテムと同一になることをテストしています。

テストを実行してみます。

1
2
3
4
5
6
7
8
9
(sam-app) $ python -m pytest tests/unit/test_handler.py
============================================================= test session starts =============================================================
platform darwin -- Python 3.6.5, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/iwata.tomoya/Documents/Project/moto/sam-app, inifile:
collected 1 item
 
tests/unit/test_handler.py .                                                                                                            [100%]
 
========================================================== 1 passed in 1.01 seconds ===========================================================

テストOKです! 外部サービスに依存せずに、pythonのコードのみでテストを実行することが可能になりました。

まとめ

いかがだったでしょうか? 今回はサーバーレスアプリで使用頻度の高いDynamoDBについてのみ調査しましたが、moto自体は他にも多くのAWSサービスに対応しています。 motoを有効活用することでテストを効率化することができると感じました。 Lambdaのテスト手法についてお悩みの方は、ぜひ一度motoを試して見て下さい!

スタートプラン