kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Testcontainers for Python と Moto を組み合わせて Boto3 を使った Python コードをテストする

Moto で AWS サービスをモックして AWS SDK for Python (Boto3) を使った Python コードをテストする場合に @mock_aws デコレータや with ブロックを使うという選択肢がある.詳しくは以下のドキュメントに載っている📝

docs.getmoto.org

また Moto には「Server Mode」というコンテナベースで起動する方法もある😀

docs.getmoto.org

普段 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 に置いてあるので参考にどうぞ〜 \( 'ω')/

github.com

関連記事

kakakakakku.hatenablog.com