サイトアイコン たーちゃんの「ゼロよりはいくらかましな」

【python】pytest + boto3 + motoでDynamoDBをモックしてテストする

今回はawsのDynamoDBに特化したお話。

実際、pytest-mockなんかを駆使すればいくらでもモッキングはできるのですが、

motoというbotoの処理をモック化してくれるライブラリがあるとのことなので、

早速使ってみた感じです。

 

 

直近ではDynamoDBをテストすることになったので、

とりあえずそれに特化したサンプルをご紹介しようかなと思います。

 

 

 

 

motoって?

Moto – Mock AWS Servicesとのことで公式はこちら

ただ注意すべきなのは、全サービスをモック化してくれるわけではないという点。

対応表はこちら

 

 

ここに載っていないサービスをモックにするのであれば、

pytestのMockFixtureを使用したりしてやるしかなさそうですね。

MockFixtureを使用したモックの仕方はこちらで記事にしてますのでご参照ください。

 

 

サンプル紹介

メインの処理

テスト対象となるメインの処理は以下になります。

import boto3
from boto3.dynamodb.conditions import Attr, Key


def moto_dynamo_sample_main(hoge_id: str) -> None:
    """サンプルアプリ"""

    dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
    dynamo_table = dynamodb.Table("sample_table")

    # レコードを取得
    response = dynamo_table.query(
        KeyConditionExpression=Key("hoge_id").eq(hoge_id),
        FilterExpression=Attr("ym").eq("202006"),
        ScanIndexForward=False,
        Limit=1,
    )

    # 取得したデータを加工して新規に追加する
    update_data = response["Items"][0]
    update_data["seq"] = update_data["seq"] + 1
    update_data["data1"] = "foobar"

    # dynamo書き込み
    dynamo_table.put_item(Item=update_data)

 

今回アクセスするDynamoのテーブルは以下のようなイメージ。

hoge_id(ハッシュキー) seq(レンジキー) ym data1
hoge 1 202006 hogehoge

 

データの取得はymでフィルターかけてレンジキー降順で1件だけ取得みたいな感じです。

要するに最新のデータを取得して、データを加工して登録するような流れ。

 

 

テスト

これをテストする処理は以下。

from moto import mock_dynamodb2
from tests.dynamo_test_data import definition_mock_dynamo_table, test_data_01
from boto3.dynamodb.conditions import Attr, Key
from app.sample_main import moto_dynamo_sample_main


class TestSample:

    @mock_dynamodb2
    def test_motoでdynamodbをモックで使用する(self):
        # モック定義
        mock_table = definition_mock_dynamo_table()
        [mock_table.put_item(Item=data) for data in test_data_01()]

        # 処理実行
        moto_dynamo_sample_main("hoge")

        # 結果確認
        response = mock_table.query(
            KeyConditionExpression=Key("hoge_id").eq("hoge"),
            FilterExpression=Attr("ym").eq("202006"),
            ScanIndexForward=False,
            Limit=1,
        )

        assert len(response["Items"]) == 1, "レコード取得件数"

        actual_data = response["Items"][0]

        assert actual_data["hoge_id"] == "hoge"
        assert actual_data["ym"] == "202006"
        assert actual_data["seq"] == 3
        assert actual_data["data1"] == "foobar"

 

 

motoを使用する際はメソッドにデコレータをつけてあげます。

今回は「@mock_dynamodb2」のようにします。

こうすることでbotoのdyanamoリソースを操作するメソッドがモックになります。

 

 

このデコレータはclassの定義にもつけることができて、

その場合はclass内のメソッド全体に対してデコレコータが効果します。

 

 

注意すべきなのは、このデコレータをsetup_methodなどの

事前処理メソッドに付与したとしても、実際のテストメソッド側に

対してデコレータが効いていないとモックにならないということです。

※僕はこれを知らずawsにテーブルを作成してしまいました・・・・。

 

 

また、事前にテーブルの作成が必要で、今回はさらにそこにテストデータも必要です。

それをテストケースに書いていくとごちゃごちゃになるので、

今回は別ファイルにしました。

import boto3


def definition_mock_dynamo_table():
    """サンプルDynamoテーブル生成"""

    mock_dynamodb = boto3.resource("dynamodb")
    mock_table = mock_dynamodb.create_table(
        TableName="sample_table",
        KeySchema=[
            {"AttributeName": "hoge_id", "KeyType": "HASH"},
            {"AttributeName": "seq", "KeyType": "RANGE"},
        ],
        AttributeDefinitions=[
            {"AttributeName": "hoge_id", "AttributeType": "S"},
            {"AttributeName": "seq", "AttributeType": "N"},
        ],
        ProvisionedThroughput={
            "ReadCapacityUnits": 5,
            "WriteCapacityUnits": 5,
        },
    )

    return mock_table


def test_data_01():
    """テストデータ"""
    item = [
        {"hoge_id": "hoge", "ym": "202006", "seq": 1, "data1": "fuga"},
        {"hoge_id": "hoge", "ym": "202006", "seq": 2, "data1": "piyo"},
    ]

    return item

 

こうしておけばテーブル定義やデータが分離できて、

テストが見やすくなりますね。

 

 

まとめ

これでテスト実行するとちゃんとdynamoテーブルがモック化されて、

実際にawsへテーブルを作りにいくことなくテストを行うことができるようになります。

 

 

motoはまだまだ他のawsサービスもモック化できるようなので、

機会があればどんどん使っていきたいと思いました。

 

 

それでは!!

 

 


にほんブログ村


人気ブログランキング

モバイルバージョンを終了