boto3をlocalstack環境でpytestする
先月localstackがついにv1.0になりました。
Announcing LocalStack 1.0 General Availability!
上記ブログを読むとGAに伴う機能強化も色々あり、
- LocalStack Extensionsを使うとLocalStackの応答をフックして任意のコードを実行できる
- データの永続性を定義しやすくなった
- ロギング強化
など、他にもLocalStack自体の開発サイクルを改善したことにも触れています。
これから自分もLocalStackを使う機会は増えそうだな、と再認識したので今回はboto3をpytestする単純なものから。
前提
[packages]
localstack = "*"
pytest = "*"
awscli-local = "*"
boto3 = "*"
botocore = "*"
$ grep -A4 localstack ~/.aws/config
[profile localstack]
source_profile = localstack
region = ap-northeast-1
output = json
$ grep -A4 localstack ~/.aws/credentials
[localstack]
region = ap-northeast-1
aws_access_key_id=local
aws_secret_access_key=local
ソースコード
ある程度の複雑さが欲しかったのでOSSから拾ってきました。
(AWS SDK for Python code examples)[https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/python]
テストコード実装の選択肢
botocore.stub.Stubber
boto3ではStubberクラスが提供されているのでそれを使ってテストコードも可能ですが、今回は紹介だけに留めます。AWS純正のテストツールなんですがコードの巨大さがよく分かります...
- 今回使うサンプルコードではbotocore.stub - Stubberを使ってます
- Stubによるテストコード実装のサンプルにもなってます
LocalStack
コンテナ環境(http://localhost:4566)を使ってAWSサービスを再現してくれるため、リクエストをstubやmockするより(コンテナに対して)実際にリクエスト応答を実行してAWSリソースのテストが可能です。
個人的にはLocalStackの開発チームが言っている the broken cloud software development model を直すという方針が好きです。この記事は実感わきました。
今回のテストコード実装例
アプリの実行環境ごとにソースコードでboto3.session.Sessionのendpoint_url
を切り替える方法もあり得ますが。が、テストコード導入のためにソースコード変更の必要が...という辛さもあるので今回は選択肢から外しました。
ソースコード
conftest.py
今回の例では
export AWS_REGION=ap-northeast-1
export AWS_PROFILE=localstack
export localstack_endpoint="http://localhost:4566"
がテストコードに必要な環境変数です。
import os
import pytest
import boto3
from botocore.config import Config
AWS_PROFILE = os.getenv('AWS_PROFILE')
AWS_REGION = os.getenv('AWS_REGION')
ENDPOINT = os.getenv('localstack_endpoint')
@pytest.fixture(scope='session', autouse=True)
def dynamodb_local():
boto3.setup_default_session(profile_name=AWS_PROFILE)
dynamodb = boto3.resource('dynamodb', region_name=AWS_REGION, endpoint_url=ENDPOINT)
# return dynamodb
# モックしたいだけなのでどちらでも可
yield dynamodb
## test_scenario_getting_started_movies.py
先に挙げたscenario_getting_started_movies
に対するテストコードです。
from unittest import mock
import boto3
import scenario_getting_started_movies
from scenario_getting_started_movies import run_scenario
def test_run_scenario(dynamodb_local):
movies_file = 'test.json'
test_Movies = scenario_getting_started_movies.Movies(dynamodb_local)
with mock.patch('scenario_getting_started_movies.Movies', return_value=test_Movies):
result = run_scenario('doc-example-table-movies', movies_file, boto3.resource('dynamodb'))
run_scenario
は引数にboto3クライアントを渡すのでdynamodb_local
をそのまま使えばお終いでしたが、上記では少しだけ汎用的な書き方を書き方をしました。
-
mock.patch
でMoviesクラスをlocalstack用にモック - withブロック内でモックされたオブジェクト値を使ってソースコードを実行
実行すると次のようになりました。
$ python -m pytest -s -v test/test_scenario_getting_started_movies.py
======================================================================================================================================================= test session starts ========================================================================================================================================================
platform darwin -- Python 3.8.9, pytest-7.1.2, pluggy-1.0.0 -- **/.venv/bin/python
cachedir: .pytest_cache
rootdir: **/aws-doc-sdk-examples/python/example_code/dynamodb/GettingStarted
collected 1 item
test/test_scenario_getting_started_movies.py::test_run_scenario ----------------------------------------------------------------------------------------
Welcome to the Amazon DynamoDB getting started demo.
----------------------------------------------------------------------------------------
Enter the title of a movie you want to add to the table: abc
What year was it released? 2999
On a scale of 1 - 10, how do you rate it? 0
0.0 must be between 1 and 10.
On a scale of 1 - 10, how do you rate it? 1
Summarize the plot for me: Secret!!
Added 'abc' to 'doc-example-table-movies'.
----------------------------------------------------------------------------------------
Let's update your movie.
You rated it 1.0, what new rating would you give it? 1
You summarized the plot as 'Secret!!'.
What would you say now? Not say again..
Updated 'abc' with new attributes:
{'info': {'plot': 'Not say again..', 'rating': Decimal('1.0')}}
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
Let's move on...do you want to get info about 'The Lord of the Rings: The Fellowship of the Ring'? (y/n) n
----------------------------------------------------------------------------------------
Let's get a list of movies released in a given year. Enter a year between 1972 and 2018: 2018
I don't know about any movies released in 2018!
Try another year? (y/n) n
----------------------------------------------------------------------------------------
Let's remove your movie from the table. Do you want to remove 'abc'? (y/n)n
----------------------------------------------------------------------------------------
Delete the table? (y/n) n
Don't forget to delete the table when you're done or you might incur charges on your account.
Thanks for watching!
----------------------------------------------------------------------------------------
PASSED
=================================================================================================================================================== 1 passed in 69.32s (0:01:09) ===================================================================================================================================================
備考
AWSサービスの初学者にlocalstack環境はすごく良さそう。
docker-composeやテストコードの学習コストが大したことないなら、実際にAWSアカウントで試行錯誤するより気軽にアプリ開発を試せます。
Discussion