Testcontainers を使うと,テストコードを実行するときに必要になるデータベース・キャッシュ・キューなどの依存関係をコード上で管理できて,実行後にはコンテナを自動的に消してくれるという使い捨て可能な仕組みを簡単に作れる❗️Testcontainers のサイトに載っている「Test dependencies as code」という表現はピッタリだと思う👌
Testcontainers は Java / Go / .NET / Rust など多くの言語をサポートしているけど,今回は Python 用の testcontainers-python を試してみた.検証に使ったコードを紹介しつつ,簡単にまとめておく✍
また Testcontainers Cloud もあったりする🌩
前提
今回は以下の前提で試す💡なお testcontainers-python は MySQL / PostgreSQL / Redis / Kafka / RabbitMQ / LocalStack など多くの依存関係をサポートしているので,組み合わせてテストコードを実行することもできる.今回は LocalStack に限定する.
- Amazon DynamoDB を操作するコードをテストする
- Amazon DynamoDB のエミュレーターとして LocalStack を使う
- テストフレームワークとして pytest を使う
- testcontainers-python の
LocalStackContainer
を使う
ディレクトリ構成は以下のようにした.特に決まってなく自由に変更できる👌
. ├── README.md ├── pyproject.toml ├── requirements-test.txt ├── src │ └── app.py ├── tests │ └── test_app.py └── venv
ドキュメント
testcontainers-python に関しては以下のドキュメントを読むとイメージがつかめると思う.
testcontainers-python.readthedocs.io
しかし,残念ながら testcontainers-python の LocalStackContainer
に関するドキュメントはほとんどなく(見つけられず)実際に GitHub のコードを読みながらメソッドなどを探したりしていた.
サンプルコード
👾 app.py
コードには特に意味はないけど,Amazon DynamoDB の Forum
テーブルからアイテムを取得する search_forum()
関数を今回のテスト対象とする.Forum
テーブルというのは Amazon DynamoDB のドキュメントに載っているサンプルでそのまま使うことにした.
そして,boto3 client は環境変数 ENV
によって3種類作れるようにしてある👌
local
: ローカル開発用 (LocalStack)test
: テスト用 (testcontainers-python / LocalStack)- その他: 実際の AWS アカウント
また testcontainers-python の LocalStackContainer
を使うと http://localhost:63033
や http://localhost:63058
など実行時のポートが変化するため,後述する test_app.py
で環境変数 TESTCONTAINERS_LOCALSTACK_ENDPOINT_URL
を設定することにした👌もっとイイ方法もありそう.ちなみに LocalStackContainer
の edge_port
は 0.0.0.0:63167->24566/tcp
のようにネットワークが構成されるため "コンテナ側のポート" を指定するオプションだった.
import boto3 import os TABLE_NAME = 'Forum' if os.environ['ENV'] == 'local': dynamodb = boto3.client('dynamodb', endpoint_url='http://localhost:4566') elif os.environ['ENV'] == 'test': dynamodb = boto3.client('dynamodb', endpoint_url=os.environ['TESTCONTAINERS_LOCALSTACK_ENDPOINT_URL']) else: dynamodb = boto3.client('dynamodb') def search_forum(name): return dynamodb.get_item( TableName=TABLE_NAME, Key={'Name': {'S': name}} )
👾 test_app.py
pytest 実行時に呼び出すフィクスチャとして @pytest.fixture
デコレータで setup()
関数を実装した.LocalStackContainer
で LocalStack のコンテナイメージを設定しているため,テストコードを実行する前に自動的に LocalStack コンテナが起動される👌ちなみに LocalStackContainer
の get_client()
関数は boto3 client を返しているため,boto3 に慣れていれば普段と同じように実装できる.
そして,Amazon DynamoDB テーブル Forum
を作って,サンプルデータ(アイテム)を1つ登録している.テスト観点によっては Faker などを使ってリアルなサンプルデータを登録すると良さそう.
最後にテストケースとしては search_forum()
関数を呼び出して「アイテムを取得できる場合」と「アイテムを取得できない場合」を確認している✔️
import os import pytest from testcontainers.localstack import LocalStackContainer TABLE_NAME = 'Forum' @pytest.fixture(scope='module', autouse=True) def setup(): with LocalStackContainer(image='localstack/localstack:3', region_name='ap-northeast-1') as localstack: os.environ['TESTCONTAINERS_LOCALSTACK_ENDPOINT_URL'] = localstack.get_url() dynamodb = localstack.get_client('dynamodb') dynamodb.create_table( TableName=TABLE_NAME, KeySchema=[ { 'AttributeName': 'Name', 'KeyType': 'HASH', } ], AttributeDefinitions=[ { 'AttributeName': 'Name', 'AttributeType': 'S', } ], BillingMode='PAY_PER_REQUEST', ) item = { 'Name': {'S': 'Amazon DynamoDB'}, 'Category': {'S': 'Amazon Web Services'}, 'Threads': {'N': '2'}, 'Messages': {'N': '4'}, 'Views': {'N': '1000'}, } dynamodb.put_item(TableName=TABLE_NAME, Item=item) yield localstack def test_search_forum(): from app import search_forum item = search_forum('Amazon DynamoDB') assert item['Item']['Category']['S'] == 'Amazon Web Services' assert item['Item']['Views']['N'] == '1000' item = search_forum('Amazon S3') assert 'Item' not in item
testcontainers-python と LocalStack の検証はザッとこんな感じ \( 'ω')/
動作確認
期待通り実行できた👏
$ ENV=test pytest -p no:warnings =========================================================================================== test session starts ============================================================================================ (中略) configfile: pyproject.toml collected 1 item tests/test_app.py . [100%] ============================================================================================ 1 passed in 4.97s =============================================================================================
現時点だと Python 3.12 で boto3 の DeprecationWarning が出るため -p no:warnings
で抑止している🛑