AWSでPython動作環境をサクッと構築したい...

AWSPython、どちらも触れるようになっておきたい!

とは思うものの学習ハードルが高いと感じてしまう。

多少なりともお金もかかるしね...

 

そこで、motoというツールを使って環境を作成するというものを見つけたのでいつか役に立つだろうということで控えておく。

 

moto について

moto は

A library that allows you to easily mock out tests based on AWS infrastructure.

とあるように、テスト用途で使用する AWS のインフラを容易に構築するためのライブラリです。AWS のリソース全てを網羅はしていません[1]が、DynamoDB や S3, SQS など Lambda と一緒に使われやすいリソースについてはカバーをしています。docker-compose を書くことなく、単体テストAWS リソースのモックアップを追加できます。やることは、モックで使用したいリソース名を pip install の時に extra で指定するだけ。

# EC2, S3 のモックをインストールする時の例
pip install moto[ec2,s3]

# 全てのモックをインストールする時
pip install moto[all]

moto の使い方

moto の利用方法は 3 つあります。

  • Decorator
  • Context Manager
  • Raw

使い方は公式に例がいくつか出ています。

それぞれの用法の良し悪しを私なりにまとめるとこんな感じになります。

用法 良い点 悪い点
Decorator 書く量が少ない モックの起動・停止を柔軟に決めるのが難しい
Context Manager with で細かくスコープを分けられ、終了処理を書く必要がない。 モックの数が多くなると工夫が必要。インデントが深くなりやすい
Raw 起動と停止の自由度が高い。fixture や setUp,tearDown 関数などと合わせて使える モックの起動と停止のタイミングが複雑になりやすい。停止処理を忘れる

ざっくりいうとこんな感じで決めるといいです。

特に希望がない時 → 各自の好みで決めたらいい
すぐに導入したい時 → Decorator
部分的に導入したい時 → Context Manager か Raw
unittest の setUp, tearDown や pytest の fixture と一緒に使いたい → Raw

 

以下では、各用法の簡単な解説とちょっとした Tips を紹介します。

Decorator

Decorator での書き方は、以下のようになります[2]

@mock_s3
def test_my_model_save():
    # Create Bucket so that test can run
    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()

    assert body == 'is awesome'

デコレーターで、テスト自体をラップしてしまって、そのスコープ内では常にモックが使えるようにする書き方です。デコレーターを複数重ねることで、複数のモックアップを同時に起動できます。unittest.mock をデコレーターで使っている人には、馴染みの深い書き方かもしれません。

複数渡すとき

この書き方を利用するのは以下のような状況が考えられそうです。

  • モックを特定のテスト関数で使う、かつその関数が実行されている間は起動されていて良い時
  • 単体テストがすでにあり、初めてモックアップを導入する時(例えば、今までテストで開発環境のリソースにアクセスしてテストしていた時など)

Context Manager

Context Manager での書き方は、以下のようになります[3]

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()

        assert body == 'is awesome'

個人的には一番好きな書き方です。with でスコープを明示的に区切り、そのスコープ内ではモックアップが起動されるようにした書き方です。with を抜けるとモックアップが停止するため、意図しないタイミングでモックが起動することを避けることができます。複数のモックアップを起動するときは、カンマ区切りで渡す方法と、contextlib.ExitStack[4] を使う方法があります。

カンマ区切りで渡す
contextlib.ExitStack を使う

Context Manager の記法は以下の時に優れていそうです。

  • 関数内でモックが起動されるタイミングを細かく制御したい時

 

moto を自分で起動し、停止する場合は以下のように書きます。

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()

    assert conn.Object('mybucket', 'steve').get()['Body'].read().decode() == 'is awesome'

    mock.stop()

startstop を用いて、任意のタイミングでモックの起動・停止を行う書き方です。定番の使われ方は、unittest.TestCase で setUp や tearDown を書く時でしょう。[6]

unittest での利用例

複数のモックを起動したいときは純粋に、必要分だけ startstop を書くだけです。

複数渡すとき

この用法は、以下のような時に優れていそうです。

  • unittest などのセットアップ処理で利用する時
  • 自分の好きなタイミングで、モックを起動・停止したい時