2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

pytestでawsサービスの単体テストを書くときはmotoが便利という話

Posted at

はじめに

awsサービスを利用した単体テストを書く時にいいライブラリはないか探していた際に見つけた物をメモしておきます。
この記事は、公式ドキュメントの書き方を紹介した後に軽いサンプルテストを書き残してあります。

また、全コードは私のリポジトリにあるので記述していない部分はそちらを参照してください。

motoとは

motoはAWSサービスを利用した単体テストを簡単にモックアウトできるライブラリです。aws公式のライブラリではないのでAWSサービス全網羅というわけではありませんが、私が使っている範囲では不自由ありませんでした。(motoのAWSサービス対応表)

例としてs3にデータを投入する簡単なコードのテストを考えます。

sample.py
import boto3

class MyModel(object):
    def __init__(self, name, value):
        self.name = name
        self.value = value

    def save(self):
        s3 = boto3.client('s3', region_name='us-east-1')
        s3.put_object(Bucket='mybucket', Key=self.name, Body=self.value)

公式を見ると3つの書き方があるようです。

  • Decorator
  • Context Manager
  • Raw

pytestと組み合わせた時には個人的に以下のように使えると思いました。

書き方 pytestでの使い方
decorator テスト内でサービスを指定したい時
context manager fixtureなどで事前にdbやバケットを準備しておきたい時
raw サービスがない時のエラーハンドリング?(いい利用方法があれば逆に教えてください)

以下書き方の紹介です。

Decorator

@mock_s3のように書く方法です。
後述しますが、pytestのテストを実行する部分でバケットやテーブルを作成するときによさそうな書き方です。

s3サービスをmockする場合は、@mock_s3を関数の前に呼び出してあげましょう。デコレータを直前に呼び出した関数で使うs3サービスはmockされます。

test_sample_1.py
import boto3
from moto import mock_s3
from mymodule import MyModel

@mock_s3
def test_my_model_save():
    conn = boto3.resource('s3', region_name='us-east-1')
    # We need to create the bucket since this is all in Moto's 'virtual' AWS account
    conn.create_bucket(Bucket='mybucket')

    model_instance = MyModel('steve', 'is awesome')
    model_instance.save()

    body = conn.Object('mybucket', 'steve').get()[
        'Body'].read().decode("utf-8")

    assert body == 'is awesome'

Context Manager

Context Managerではwith mock_s3():でくくった部分内でs3サービスがモックされます。

後述しますが、pytestのfixture部分に書く時はcontext managerを使うのが便利そうです。

test_sample_2.py
def test_my_model_save():
    with mock_s3():
        conn = boto3.resource('s3', region_name='us-east-1')
        conn.create_bucket(Bucket='mybucket')

        model_instance = MyModel('steve', 'is awesome')
        model_instance.save()

        body = conn.Object('mybucket', 'steve').get()[
            'Body'].read().decode("utf-8")

        assert body == 'is awesome'

Raw

Rawの書き方はmockの開始と終了を手動で指定することができます。

mock.stop()を描き忘れないように注意しないといけないですが、開始と終了のタイミングを自由に指定できます。

def test_my_model_save():
    mock = mock_s3()
    mock.start()

    conn = boto3.resource('s3', region_name='us-east-1')
    conn.create_bucket(Bucket='mybucket')

    model_instance = MyModel('steve', 'is awesome')
    model_instance.save()

    body = conn.Object('mybucket', 'steve').get()[
        'Body'].read().decode("utf-8")

    assert body == 'is awesome'

    mock.stop()

pytestで実践

公式の書き方がわかったので、dynamodbのテーブルが作成されていることを確認するテストをcontext managerとdecoratorで試します。

環境構築

筆者はpipenvを使った環境構築が楽で好きなのでpipenvを使います。

requirements.txt
boto3
moto
pytest

requirements.txtを準備してpipenv installを行います。

% pipenv --python 3.9
% pipenv install -r requirements.txt

自分が書いたコードの階層構造は以下のようになっています。

% tree
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── requirements.txt
├── sample
│   ├── __init__.py
│   └── lambda_function.py
└── test
    ├── conftest.py
    ├── test_main.py
    └── utils.py

conftestでdynamodbのテーブルを準備する

pytestのfixureという機能を使えばテスト実行のための前処理を外だしすることができます。

conftest.py
import os
import pytest

import boto3
from moto import mock_dynamodb

def set_env():
    """公式推奨のenv設定
    万が一にも本番環境へ変更が当たらないようにする
    """
    os.environ['AWS_ACCESS_KEY_ID'] = 'test'
    os.environ['AWS_SECRET_ACCESS_KEY'] = 'test'
    os.environ['AWS_DEFAULT_REGION'] = 'ap-northeast-1'
    os.environ['AWS_SECURITY_TOKEN'] = 'test'
    os.environ['AWS_SESSION_TOKEN'] = 'test'


@pytest.fixture
def create_db_by_context_manager():
    set_env()

    with mock_dynamodb():
        dynamodb = boto3.client('dynamodb')
        table = dynamodb.create_table(
            TableName='test_table',
            KeySchema=[
                {
                    'AttributeName': 'id',
                    'KeyType': 'HASH'
                }
            ],
            AttributeDefinitions=[
                {
                    'AttributeName': 'id',
                    'AttributeType': 'S'
                },
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 10,
                'WriteCapacityUnits': 10
            }
        )

        yield table

確認のため、fixtureで作ったtest_tableが作成されているかを確認します。

test_main.py
...

def test_context_managerでtest_tableが作られているか確認する(create_db_by_context_manager):
    dynamodb_client = boto3.client('dynamodb')
    res = dynamodb_client.describe_table(TableName='test_table')
    assert get_table_name_from_res(res) == 'test_table'

...

テスト内部でdynamodbを準備する

次はデコレータでテスト内部でdynamodbをモックすることを考えます。

dynamodbのmockを利用するテストで@mock_dynamodbを呼び出して使います。デコレータを使う場合は、デコレータを使った部分でテーブルを作る必要があります。

test_main.py
@mock_dynamodb
def test_decoratorでtest_tableが作られているか確認する():
    create_table()
    dynamodb_client = boto3.client('dynamodb')
    res = dynamodb_client.describe_table(TableName='test_table')
    assert get_table_name_from_res(res)  == 'test_table'

さいごに

motoを使えば簡単にawsサービスを使ったアプリケーションの単体テストができそうです。世の中便利なライブラリがたくさんあって助かります。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?