kakakakakku blog

Weekly Tech Blog: Keep on Learning!

localstack-utils: 単体テスト実行時に使い捨て可能な LocalStack を起動しよう

LocalStack から公式に提供されている localstack-utils を使うと,pytest など Python で単体テストを実行するときに一時的な(使い捨て可能な)LocalStack 環境を起動できる🌍

docs.localstack.cloud

ちなみに僕は普段仕事で testcontainers-pythonLocalStackContainer を使ってて最高に便利なんだけど,結果的に localstack-utils もほとんど同じように使うことができた👌

kakakakakku.hatenablog.com

現在最新は localstack-utils 1.0.1 だった.

github.com

pypi.org

サンプルコード

👾 app.py

まずは Amazon DynamoDB の Forum テーブルからアイテムを取得する search_forum() 関数を今回のテスト対象とする.Forum テーブルというのは Amazon DynamoDB のドキュメントに載っているサンプルでそのまま使うことにした.

そして,boto3 client は環境変数 ENV によって3種類作れるようにしてある👌

  • local: ローカル開発用 (LocalStack)
  • test: テスト用 (localstack-utils / LocalStack)
  • その他: 実際の AWS アカウント

ちなみに localstack-utils のデフォルト設定では 4566 ポートで LocalStack を起動する.それだとローカル開発用の LocalStack とポートが競合してしまうため,今回は 14566 ポートで起動することにした.

import os

import boto3

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='http://localhost:14566')
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() 関数を実装した.localstack-utils には LocalStack を起動する startup_localstack() 関数と LocalStack を停止する stop_localstack() 関数が実装されているため,それを _setup() 関数内で実行している.14566 ポートで起動する設定もしてある👌

そして,Amazon DynamoDB テーブル Forum を作って,サンプルデータ(アイテム)を1つ登録している.最後にテストケースとしては search_forum() 関数を呼び出して「アイテムを取得できる場合」「アイテムを取得できない場合」を確認している✔️

import boto3
import pytest
from app import search_forum
from localstack_utils.localstack import startup_localstack, stop_localstack

TABLE_NAME = 'Forum'


@pytest.fixture(scope='module', autouse=True)
def _setup():
    startup_localstack(gateway_listen='0.0.0.0:14566')

    dynamodb = boto3.client('dynamodb', endpoint_url='http://localhost:14566')

    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

    stop_localstack()


def test_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

動作確認

期待通り実行できた👏

$ 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 =============================================================================================

詳しくはコード参照

localstack-utils にはドキュメントというドキュメントはなく,オプションなど詳しくは GitHub のコードを確認する必要がある.例えば今回は startup_localstack() 関数に gateway_listen を設定したけど,他にも image_name / tag / pro なども設定できる👌

github.com

まとめ

localstack-utils を使って Python で単体テストを実行するときに一時的な(使い捨て可能な)LocalStack 環境を活用しよう❗️