はじめに

今回はAWSサービスをテストするためのライブラリ「moto」について紹介します。
motoの魅力は、実際のAWSサービスを使わずにテストを行えるため、コスト削減や安全なテストに役立つ点にあります。

なお、pytestのmockを使用してAmazon S3への操作をローカルでテストする方法については
こちらをご覧ください。

motoとは

motoは、AWSのサービスをモック化するためのPythonライブラリです。boto3と互換性があり、
実際のAWSサービスを使わずにAWS SDKのAPIをエミュレートすることができます。
このライブラリの最大の特徴は、実際のAWSサービスのような振る舞いをローカル環境で再現できる点にあります。

motoのインストールと基本設定

インストール方法

motoは、Pythonのパッケージマネージャーであるpipを使用して簡単にインストールできます。

pip install moto

特定のAWSサービスのみをモック化する場合は、そのサービスのみをインストールすることも可能です。
これは依存関係を最小限に抑えたい場合に便利です。

pip install moto[s3]  # S3サービスのみをサポート
pip install moto[dynamodb]  # DynamoDBサービスのみをサポート

すべてのAWSサービスをサポートする完全版をインストールする場合は以下を使用します。

pip install moto[all]

なお、pip install motopip install moto[all]の違いについてですが、
基本的なmotoライブラリは同じものがインストールされます。
moto[all]は追加の依存関係(すべてのAWSサービスをサポートするために必要なライブラリ)も
一緒にインストールするオプションです。
特定のサービスだけを使用する場合はmoto[s3]のように個別にインストールすることで、
不要な依存関係を避けることができます。

また、pytestと組み合わせて使用する場合は、pytestもインストールしておく必要があります。

pip install pytest

サポートされている主要AWSサービス

motoは多くのAWSサービスをサポートしています。私が使ったことがあるのは以下のサービスです。

  • S3 (Simple Storage Service)
  • DynamoDB
  • EC2
  • Lambda
  • IAM
  • SQS (Simple Queue Service)
  • SNS (Simple Notification Service)

他にも、RDS、CloudFormationなど多くのサービスがサポートされているようです。
各サービスのサポート状況や完全なリストについては、
motoの公式GitHubを参照してください。

motoの基本的な仕組み

通常はboto3を介してAWSサービスを呼び出しますが、motoではAWSサービスにリクエストを送信せずに
モック環境で処理を行います。それを実現するために@mock_awsデコレータを使用します。

@mock_awsデコレータを使用することで、boto3を介したAWSサービスへの呼び出しが
すべてインターセプトされます。
インターセプトとは、実際のAWSサービスにリクエストを送信せずに、
代わりにmotoが提供するモック環境でそのリクエストを処理することを意味します。

具体的には以下のような動作が行われます。

  • boto3クライアントやリソースを使用して行われる操作が実際のAWSではなくmotoのモック環境で処理されます。
    (例:バケットの作成、オブジェクトのアップロードなど)
  • モック環境では、AWSサービスの動作をエミュレートするため、
    実際のAWSサービスを使用する場合と同じコードでテストを実行できます。
  • これにより、AWSリソースを実際に作成することなく、
    ローカルで安全かつコストをかけずにテストを行うことが可能です。

例えば、以下のコードでは、s3.create_bucketがmotoのモック環境で処理されます。

@mock_aws
def test_upload_file_to_s3_success_moto():
    s3 = boto3.client("s3", region_name="us-east-1")
    bucket_name = "test-bucket"
    s3.create_bucket(Bucket=bucket_name)
    # 実際のAWSではなく、モック環境でバケットが作成される

motoの使用パターン

テストのシナリオに応じて使い分けるべきです。最適なパターンは状況によって異なります。

1. デコレータとしての使用

@mock_awsデコレータを使用する方法です。これにより、テスト関数内のすべてのAWS APIコールが
自動的にモック化されます。
@mock_s3デコレータを使用して、S3用のデコレータを使用することもできます。

なお、@mock_s3デコレータの場合、モックが使われるのはS3のみで、
S3以外のサービスはモックではなく実際のAWSサービスが呼ばれます。
特定のサービスだけをモック化したい場合は、このような個別のデコレータを使用すると便利です。

from moto import mock_aws
import boto3

@mock_aws
def test_s3_operation():
    # このテスト関数内のすべてのAWS操作がモック化される
    s3 = boto3.client('s3', region_name='us-east-1')
    s3.create_bucket(Bucket='test-bucket')
    # テストコード...

この方法は最もシンプルで使いやすい方法です。テスト関数単位でAWSモックが独立しているため、
テスト間の独立性が保たれます。

2. コンテキストマネージャとしての使用

特定のコードブロック内でのみAWS APIコールをモック化したい場合は、
コンテキストマネージャ with mock_aws(): を使用できます。
複雑なテスト内で部分的にAWSモックが必要な場合に、この方法を使うことがあります。

from moto import mock_aws
import boto3

def test_s3_with_context_manager():
    # コンテキストマネージャとしてmotoを使用
    with mock_aws():
        # このブロック内でのAWS操作のみがモック化される
        s3 = boto3.client('s3', region_name='us-east-1')
        bucket_name = 'context-bucket'

        # バケット作成
        s3.create_bucket(Bucket=bucket_name)

        # オブジェクト作成とテスト
        s3.put_object(Bucket=bucket_name, Key='example.txt', Body='Example content')
        response = s3.get_object(Bucket=bucket_name, Key='example.txt')
        content = response['Body'].read().decode('utf-8')
        assert content == 'Example content'

    # コンテキストブロックを出ると、モックが解除される

この方法は、テスト内の一部分だけでモックが必要な場合に便利です。

motoを使用したS3テストの例

S3のアップロード機能テスト

以下は、motoライブラリを使用してS3のアップロード機能をテストするコードの例です。
このコードでは、AWS S3サービスをモック化し、実際のAWS環境を使用せずにテストを実行します。

# [test_s3_upload_moto.py](http://_vscodecontentref_/0)
import os
import pytest
import boto3
from moto import mock_aws
from s3_upload import upload_file_to_s3

@mock_aws
def test_upload_file_to_s3_success_moto():
    """S3アップロードが成功するケースのテスト(moto使用)"""
    # モックS3サービスの準備
    s3 = boto3.client("s3", region_name="us-east-1")
    bucket_name = "test-bucket"
    s3.create_bucket(Bucket=bucket_name)

    # テスト用のファイルを作成
    test_file_path = "test_file.txt"
    with open(test_file_path, "wb") as f:
        f.write(b"test file content")

    # テスト実行
    result = upload_file_to_s3(test_file_path, bucket_name, "test-key.txt")

    assert result is True

    # S3にオブジェクトが作成されたことを確認
    response = s3.get_object(Bucket=bucket_name, Key="test-key.txt")
    content = response["Body"].read()
    assert content == b"test file content"

motoを使用したテストの魅力は、@mock_awsデコレータを使用してS3サービスをモック化し、
実際のboto3 APIをそのまま使用してテストを書ける点です。
実際のAWSサービスを使用するときとほぼ同じ感覚でコードを書けるため、
テストと本番コードの差異が少なくなります。

S3のバケットポリシーとバージョニングのテスト

import json
import boto3
import pytest
from moto import mock_aws

@mock_aws
def test_s3_bucket_policy_and_versioning():
    # S3クライアント作成
    s3 = boto3.client("s3", region_name="us-east-1")
    bucket_name = "versioned-bucket"

    # バケット作成
    s3.create_bucket(Bucket=bucket_name)

    # バージョニングを有効化
    s3.put_bucket_versioning(
        Bucket=bucket_name,
        VersioningConfiguration={"Status": "Enabled"}
    )

    # バケットポリシーの設定
    bucket_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "PublicRead",
                "Effect": "Allow",
                "Principal": "*",
                "Action": ["s3:GetObject"],
                "Resource": [f"arn:aws:s3:::{bucket_name}/*"]
            }
        ]
    }
    s3.put_bucket_policy(
        Bucket=bucket_name,
        Policy=json.dumps(bucket_policy)
    )

    # 設定を確認
    versioning_response = s3.get_bucket_versioning(Bucket=bucket_name)
    policy_response = s3.get_bucket_policy(Bucket=bucket_name)

    assert versioning_response["Status"] == "Enabled"
    assert json.loads(policy_response["Policy"]) == bucket_policy

    # オブジェクトの複数バージョンを作成
    s3.put_object(Bucket=bucket_name, Key="test.txt", Body="version 1")
    v1_response = s3.get_object(Bucket=bucket_name, Key="test.txt")
    v1_version_id = v1_response["VersionId"]

    s3.put_object(Bucket=bucket_name, Key="test.txt", Body="version 2")
    v2_response = s3.get_object(Bucket=bucket_name, Key="test.txt")
    v2_version_id = v2_response["VersionId"]

    # 特定バージョンのオブジェクトを取得
    v1_content = s3.get_object(
        Bucket=bucket_name, 
        Key="test.txt", 
        VersionId=v1_version_id
    )["Body"].read().decode()

    assert v1_content == "version 1"
    assert v1_version_id != v2_version_id

この例では、単純なオブジェクトの作成・読み取りだけでなく、
バケットポリシーの設定やバージョニングなど、より高度な機能もテストしています。
このようなより複雑なシナリオにも対応できることが、motoの推しポイントです。

エラーケースのテスト

motoでは、AWSサービスの様々なエラーケースもテストできます。
実際のAWS環境と同様にエラーレスポンスを返してくれるので、
例外処理のテストも本番に近い形で行えるのが便利です。

import pytest
import boto3
from moto import mock_aws
from botocore.exceptions import ClientError

@mock_aws
def test_s3_error_cases():
    """S3のエラーケースのテスト"""
    # S3クライアント作成
    s3 = boto3.client('s3', region_name='us-east-1')

    # 存在しないバケットからのオブジェクト取得は失敗する
    with pytest.raises(ClientError) as e:
        s3.get_object(Bucket='non-existent-bucket', Key='test.txt')

    # エラーコードを確認
    assert e.value.response['Error']['Code'] == 'NoSuchBucket'

    # バケット作成
    bucket_name = 'test-bucket'
    s3.create_bucket(Bucket=bucket_name)

    # 存在しないオブジェクトの取得も失敗する
    with pytest.raises(ClientError) as e:
        s3.get_object(Bucket=bucket_name, Key='non-existent-object.txt')

    # エラーコードを確認
    assert e.value.response['Error']['Code'] == 'NoSuchKey'

このような例外処理のテストも、実際のAWSサービスと同様のエラーコードで動作するので、
例外ハンドリングのテストがしやすいです。

moto使用のコツ

motoを使う上で役立つ実践方法をいくつか紹介します。

1. テスト環境のセットアップを共通化する

多くのテストで同じようなセットアップが必要な場合は、
pytestのフィクスチャ @pytest.fixture を使用して共通化すると便利です。

import pytest
import boto3
from moto import mock_aws

@pytest.fixture
def s3_client():
    with mock_aws():
        s3 = boto3.client('s3', region_name='us-east-1')
        yield s3

@pytest.fixture
def test_bucket(s3_client):
    bucket_name = 'test-bucket'
    s3_client.create_bucket(Bucket=bucket_name)
    yield bucket_name

def test_s3_upload(s3_client, test_bucket):
    s3_client.put_object(Bucket=test_bucket, Key='test.txt', Body='Hello World')
    response = s3_client.get_object(Bucket=test_bucket, Key='test.txt')
    content = response['Body'].read().decode('utf-8')
    assert content == 'Hello World'

このように、フィクスチャを使うことでテストコードがスッキリして読みやすくなります。

2. リージョンの指定

motoを使用する際は常にリージョンを明示的に指定することが重要です。
テストの再現性が向上し、環境によって異なるリージョン設定による予期しない動作を防ぐことができます。

# 良い例
s3 = boto3.client('s3', region_name='us-east-1')

# 避けるべき例
s3 = boto3.client('s3')  # リージョンが明示されていない

3. テストの分離と独立性

各テストは互いに独立して実行できるように設計することが重要です。
テスト間で状態を共有しないようにすることで、テストの信頼性が高まり、デバッグも容易になります。

まとめ

この記事では、motoライブラリについて紹介しました。
motoを使うと、AWSサービスをモック化して、実際のAWS環境を使用せずにテストを行うことができます。

motoの最大の魅力は実際のAWS環境に近い形でテストができる点です。
実際のboto3 APIをそのまま使用してテストを書けるため、
テストコードと本番コードの差異が少なく、より信頼性の高いテストが可能になります。

AWSサービスを使用するアプリケーションのテストにおいて、motoは非常に有用なツールです。
ぜひ試してみてください