MinIOを使ってS3にアクセスするpytestの単体テストを書く
pytestでS3にアクセスする処理のテストを書くときは、
motoのmock_s3を利用することが一般的だと思います。
Moto - Mock AWS Services | GitHub
https://github.com/spulec/moto
テスト対象のライブラリ依存と、motoのライブラリ依存が干渉して、
motoが利用出来ないケースに遭遇したので、
このエントリでは、MinIOを使って単体テストを書いてみます。
MinIO | High Performance, Kubernetes Native Object Storage
https://min.io/
MinIOのインストール
まずはMinIOをインストールします、
以下のページを参照して、環境にあった手順でインストールします。
Download | MinIO
https://min.io/download#/kubernetes
MacOSでhomebrewを使っている場合は、次の通り。
brew install minio/stable/minio
インストールできたかを確認します。
$ minio --version
minio version RELEASE.2022-07-06T20-29-49Z (commit-id=8d98282afd1f2e6fd3bafad70a0f63b059fd91ed)
Runtime: go1.18.3 darwin/amd64
License: GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>
Copyright: 2015-2022 MinIO, Inc.
pytest, boto3のインストール
pytest, boto3をインストールします。
pip install pytest
pip install boto3
テストの実装
テスト対象・テストコードを作成します。
テスト対象は、
s3からファイルを取得してその内容を返却する関数 get_object
として、
次のように実装します。
target.py
import boto3
def get_object(bucket, key, s3cli_args=None):
if not s3cli_args:
s3cli_args = {}
s3cli = boto3.client("s3", **s3cli_args)
resp = s3cli.get_object(
Bucket=bucket,
Key=key
)
return resp["Body"].read().decode('utf-8')
テストコードでは、
scopeをsessionとした、fixtureでMinIOを起動させます。
テスト関数側では、fixtureから渡されたパラメータを使って、MinIOに接続します。
次のように実装します。
tests/test_target.py
import contextlib
import hashlib
import os
import socket
import subprocess
import tempfile
import boto3
import pytest
from target import get_object
def find_minio_bin():
# minioのパス特定
which_minio = subprocess.run(["which", "minio"], capture_output=True)
minio_bin = which_minio.stdout.decode('utf-8').strip()
return minio_bin
@pytest.fixture(scope="session")
def minio():
# minioのパス特定
minio_bin = find_minio_bin()
# 空きポートの特定
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
sock.bind(('', 0))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
port = sock.getsockname()[1]
# minioの起動
with tempfile.TemporaryDirectory() as tmpdir:
print(minio_bin)
proc = subprocess.Popen([minio_bin, 'server', tmpdir, "--address", f":{port}"])
minio_args = {
"endpoint_url": f"http://127.0.0.1:{port}",
"aws_access_key_id":
os.environ["MINIO_ROOT_USER"] if "MINIO_ROOT_USER" in os.environ else "minioadmin",
"aws_secret_access_key":
os.environ["MINIO_ROOT_PASSWORD"] if "MINIO_ROOT_PASSWORD" in os.environ else "minioadmin"
}
yield minio_args
proc.kill()
@pytest.mark.skipif(not find_minio_bin(), reason="minio not found")
def test_get_object(tmpdir, minio):
# 準備
# - テスト毎にbucketを作る
# ※ tmpdirはテスト毎にbucket名をユニークにするためだけに使っています
test_bucket = hashlib.sha224(str(tmpdir).encode()).hexdigest()
s3cli = boto3.client("s3", **minio)
s3cli.create_bucket(Bucket=test_bucket, CreateBucketConfiguration={'LocationConstraint': test_bucket})
# - オブジェクトをupload
s3cli.put_object(
Bucket=test_bucket,
Key="foo",
Body="body string"
)
# 呼び出し
resp_body = get_object(test_bucket, "foo", minio)
# 検証
assert resp_body == "body string"
テストは、次のように実行できます。
pytest tests/test_target.py
この手法を使えば、
S3(MinIO)に限らず、依存するシステムをpytestで制御して単体テストで利用できます。
例えば、redisやdynamodblocalなども同様の方法でテストできると思います。
以上。