motoをつかってboto3(AWS PythonSDK)スクリプトのテストを自動化する

動機

前回、LocalStackを使ってboto3のテストを実行してみて、リソース操作系スクリプトの動作確認を気軽に行えるようになった。

しかし、LocalStackは、EC2などのサービスには(2018年2月現在)未対応であったり、異常系のテストがエミュレートできないものが あったりと、現場で使うにはまだ改善点が多いことが分かったので、別の方法も考えてみることに。

で、情報を探してみたら、moto github.com

、、、というboto3のエミュレートを行えるライブラリを発見したので、使ってみた。

バージョン情報

Python 3.6.3

boto3 1.4.7

moto 1.2.0

対象のスクリプト

こんな感じの、「S3バケットを作成し、コマンドの実行結果を標準出力に出力する」ような スクリプトのテストを考えてみる。

import boto3
from botocore.exceptions import ClientError

"""
  S3 にバケットを作成します。
"""

bucket_name = 'kudarizakawonobore'

SUCCESS = 0
ERROR = 1

def main():
    client = boto3.client('s3')
    try:
        client.create_bucket(Bucket=bucket_name ,
                             CreateBucketConfiguration={
                                 'LocationConstraint': 'ap-northeast-1'
                                 }
                             )
        print('SUCCESS: バケットを作成しました')
        return SUCCESS
    except (ClientError) as e:
        print(e.response['Error']['Message'])
        error_code = e.response['Error']['Code']
        if error_code == 'BucketAlreadyExists':
            print('ERROR: 他の人が使っている名前です')
            return ERROR
        else:
            print('予期せぬエラー')
            raise e

if __name__ == '__main__':
    response = main()
    exit(response)

テスト側

上記スクリプトを、正常系、異常系含めてテストするには、下記のようなテストを書く。

import boto3
import src.create_bucket as target
import unittest
from io import StringIO
from unittest.mock import patch
from moto import mock_s3

class TestCreateS3(unittest.TestCase):

    @mock_s3
    def test_main(self):
        """正常系"""
        with patch('sys.stdout', new_callable=StringIO) as out:
            self.assertIs(target.main(), target.SUCCESS)
            print(out.getvalue())
            self.assertTrue(out.getvalue().find('SUCCESS:'))

    @mock_s3
    def test_main_error_bucket_already_exists(self):
        """すでに存在する場合はメッセージを表示して、ERRORコードを返す"""
        client = boto3.client('s3')
        client.create_bucket(Bucket=target.bucket_name,
                             CreateBucketConfiguration={
                                 'LocationConstraint': 'ap-northeast-1'
                             }
                             )
        with patch('sys.stdout', new_callable=StringIO) as out:
            self.assertIs(target.main(), target.ERROR)
            self.assertTrue(out.getvalue().find('ERROR: '))

if __name__ == '__main__':
    unittest.main()

やってること

motoを使うために必要な操作は二つで、テストケースでfrom moto import mock_s3motoをインポートするのと、 テスト用のメソッドに、 @mock_s3 とデコレータを付与すること。これだけ。

@mock_s3 のデコレータを付与されたメソッド内では、S3 関連の操作がすべてモック化される。 正常系のテストtest_mainでは、S3バケットが一つも存在しない空間にS3のバケットを作成しにいくので、正常終了する。

一方、異常系のテストtest_main_error_bucket_already_existsでは、一度同名のバケットを作成してからスクリプトを 実行しているので、ClientErrorが発生して異常終了する。

ちょっとした問題点

異常系のパターンをすべて網羅しているわけではない。

例えば、S3のCreateBucketでは、BucketAlreadyOwendByYou のエラーが発生するパターンがエミュレートできない。

具体的にいうと、本来 motoを使わずスクリプトを2回実行する(すでに存在するバケットを再度作成する)と、BucketAlreadyExists(他の人が使っている)ではなく、BucketAlreadyOwnedByYou(自分がすでに所有している)エラーが発生するべきである。しかし、test_main__error_bucket_already_exists で記述している通り、同一のコンテキストでバケットを2回作成したとしても、発生するエラーはBucketAlreadyExists となってしまう。 (motoのソースコードを少し見てみたが、 BucketAlreadyOwendByYouのエラーを発生させることはできなさそう。。。)

そのため、create_bucketを行う前に、同名のバケットが存在しないか事前チェックを行うなど、実装側の方でカバーしたり、テストの書き方を工夫する必要がある。

LocalStackとの住み分け

このぐらい単純なスクリプトのテストだと、motoの方が簡単にテストできるので優れていそうだが、大量のリソースを操作するスクリプトや、CloudFormationの動作確認などでは、ダッシュボード機能でリソースの状態を確認できるLocalStackの方が活躍する場面が増えてくる(と思う)

どちらもまだ使いこなせていないので、引き続き学習していきます。