kakakakakku blog

Weekly Tech Blog: Keep on Learning!

testcontainers-python: pytest 実行時に使い捨て可能な LocalStack を起動する

Testcontainers を使うと,テストコードを実行するときに必要になるデータベース・キャッシュ・キューなどの依存関係をコード上で管理できて,実行後にはコンテナを自動的に消してくれるという使い捨て可能な仕組みを簡単に作れる❗️Testcontainers のサイトに載っている「Test dependencies as code」という表現はピッタリだと思う👌

testcontainers.com

Testcontainers は Java / Go / .NET / Rust など多くの言語をサポートしているけど,今回は Python 用の testcontainers-python を試してみた.検証に使ったコードを紹介しつつ,簡単にまとめておく✍

また Testcontainers Cloud もあったりする🌩

testcontainers.com

前提

今回は以下の前提で試す💡なお testcontainers-python は MySQL / PostgreSQL / Redis / Kafka / RabbitMQ / LocalStack など多くの依存関係をサポートしているので,組み合わせてテストコードを実行することもできる.今回は LocalStack に限定する.

  • Amazon DynamoDB を操作するコードをテストする
  • Amazon DynamoDB のエミュレーターとして LocalStack を使う
  • テストフレームワークとして pytest を使う
  • testcontainers-pythonLocalStackContainer を使う

ディレクトリ構成は以下のようにした.特に決まってなく自由に変更できる👌

.
├── README.md
├── pyproject.toml
├── requirements-test.txt
├── src
│   └── app.py
├── tests
│   └── test_app.py
└── venv

ドキュメント

testcontainers-python に関しては以下のドキュメントを読むとイメージがつかめると思う.

testcontainers-python.readthedocs.io

testcontainers.com

しかし,残念ながら testcontainers-python の LocalStackContainer に関するドキュメントはほとんどなく(見つけられず)実際に GitHub のコードを読みながらメソッドなどを探したりしていた.

github.com

サンプルコード

👾 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:63033http://localhost:63058 など実行時のポートが変化するため,後述する test_app.py で環境変数 TESTCONTAINERS_LOCALSTACK_ENDPOINT_URL を設定することにした👌もっとイイ方法もありそう.ちなみに LocalStackContaineredge_port0.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 コンテナが起動される👌ちなみに LocalStackContainerget_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 で抑止している🛑

github.com