Moto で AWS サービスをモックして AWS SDK for Python (Boto3) を使った Python コードをテストする場合に @mock_aws
デコレータや with
ブロックを使うという選択肢がある.詳しくは以下のドキュメントに載っている📝
また Moto には「Server Mode」というコンテナベースで起動する方法もある😀
普段 pytest を使って Python コードをテストする場合は MySQL や LocalStack などの依存関係を Testcontainers for Python で管理していて便利なので,Moto も使えたら良いのにな〜と思っていた💡残念ながら Testcontainers for Python は Moto をサポートしていないけど,任意のコンテナイメージを起動できる DockerContainer
があって,試してみることにした.結果的にイメージ通りに Testcontainers for Python と Moto を組み合わせることができた❗️
testcontainers-python.readthedocs.io
サンプルコード
👾 src/mymodel.py
今回はサンプルとして Moto のドキュメントに載っていた MyModel
クラスを少し修正してテスト対象にする.save()
関数を実行すると Amazon S3 にファイルをアップロードするシンプルな実装になっている💡
そして,Boto3 の Amazon S3 クライアントは環境変数 ENV
によって3種類作れるようにしてある👌
local
: ローカル開発用 (Moto)test
: テスト用 (testcontainers-python / Moto)- その他: 実際の AWS アカウント
import os import boto3 if os.environ['ENV'] == 'local': s3 = boto3.client('s3', region_name='us-east-1', endpoint_url='http://localhost:5000') elif os.environ['ENV'] == 'test': s3 = boto3.client( 's3', region_name='us-east-1', endpoint_url=f'http://localhost:{os.environ['TESTCONTAINERS_MOTO_PORT']}' ) else: s3 = boto3.client('s3', region_name='us-east-1') class MyModel: def __init__(self, name, value): self.name = name self.value = value def save(self): s3.put_object(Bucket='mybucket', Key=self.name, Body=self.value) if __name__ == '__main__': model_instance = MyModel('steve', 'is awesome') model_instance.save()
👾 tests/test_mymodel.py
テストコードでは,まずフィクスチャで Testcontainers for Python の DockerContainer
を使って Moto のコンテナ (motoserver/moto) を起動していて,Amazon S3 バケット mybucket
も追加している.なお Testcontainers for Python で Moto を起動するとポートはランダムに決まるため,環境変数 TESTCONTAINERS_MOTO_PORT
に設定して src/mymodel.py
でも参照できるようにしている👌
そして,実際のテストでは save()
関数によってアップロードされた Amazon S3 オブジェクトを取得して,中身を確認している.
import os import boto3 import pytest from testcontainers.core.container import DockerContainer @pytest.fixture(scope='module', autouse=True) def _setup(): with DockerContainer('motoserver/moto').with_exposed_ports(5000) as container: os.environ['TESTCONTAINERS_MOTO_PORT'] = container.get_exposed_port(5000) s3 = boto3.client( 's3', region_name='us-east-1', endpoint_url=f'http://localhost:{os.environ['TESTCONTAINERS_MOTO_PORT']}' ) s3.create_bucket(Bucket='mybucket') yield def test_my_model_save(): from mymodel import MyModel, s3 model_instance = MyModel('steve', 'is awesome') model_instance.save() body = s3.get_object(Bucket='mybucket', Key='steve')['Body'].read().decode('utf-8') assert body == 'is awesome'
動作確認
今回は Python プロジェクトを uv で管理しているけど,環境変数 ENV
を設定して pytest を実行するとイメージ通りに動いた👌pytest 実行時に一時的に Moto が起動して,テスト終了後は自動的に削除される.便利〜 \( 'ω')/
$ ENV=test uv run pytest -p no:warnings --verbose tests ============================================================================================================================= test session starts ============================================================================================================================== (中略) configfile: pyproject.toml collected 1 item tests/test_mymodel.py::test_my_model_save PASSED [100%] ============================================================================================================================== 1 passed in 1.48s ===============================================================================================================================
GitHub Actions でテストを実行する
ついでに GitHub Actions でも pytest を実行できるようにしてみた👌
👾 .github/workflows/pytest.yml
name: Run pytest on: workflow_dispatch: push: branches: - main env: AWS_PROFILE: moto jobs: pytest: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v3 - name: Setup Python run: uv python install - name: Install dependencies run: uv sync - name: Set AWS Profile for Moto run: | aws configure set --profile moto aws_access_key_id DUMMY aws configure set --profile moto aws_secret_access_key DUMMY - name: Run pytest run: ENV=test uv run pytest -p no:warnings --verbose tests
まとめ
Testcontainers for Python と Moto を組み合わせて pytest を実行できるようにしてみた❗️これは結構便利かな〜と思う.
ちなみにコードは GitHub に置いてあるので参考にどうぞ〜 \( 'ω')/