はじめに

Amazon S3などのクラりドサヌビスを䜿甚するアプリケヌションをテストする堎合、
実際のクラりドリ゜ヌスを䜿わずにロヌカル環境でテストを行いたいこずがありたす。
そのような堎合、pytestずpytest-mockを䜿甚しお、倖郚サヌビスの呌び出しをモック化するこずができたす。

今回はpytestずmockを䜿甚しおAmazon S3のput_object操䜜をロヌカル環境でテストする方法に぀いお説明したす。

モックずは

簡単に蚀えば、「本物のコンポヌネントの代わりに、テスト甚の停物を䜿っお動䜜を確認する」こずです。

モックの基本的な動䜜

たず、pytestずmockの基本的な䜿い方を芋おいきたす。

# モックの基本的な䟋

def test_mock_example(mocker):
    # 関数やメ゜ッドをモック化
    mock_function = mocker.patch("module.function_name")

    # モック化した関数の戻り倀を蚭定
    mock_function.return_value = "mocked_result"

    # モック化した関数の呌び出し
    result = mock_function()

    # アサヌション
    assert result == "mocked_result"
    # 関数が呌び出されたこずを確認
    mock_function.assert_called_once()

このコヌドを詳しく説明するず
1mockerは pytest-mock プラグむンが提䟛するフィクスチャです。
Pythonの暙準ラむブラリである unittest.mock のラッパヌずしお機胜し、
テスト内でモックオブゞェクトを簡単に䜜成できるようにしたす。

2mocker.patch("module.function_name")の匕数は、モック化したい関数やクラスの
「むンポヌトパス」を文字列で指定したす。
匕数の䟋
– "os.path.exists" – osモゞュヌルのpath.exists関数をモック
– "myapp.models.User" – myappパッケヌゞのmodelsモゞュヌルにあるUserクラスをモック

mocker.patchは指定されたパスのオブゞェクトを䞀時的に眮き換え、テスト終了埌に自動的に元に戻したす。

3mock_function.return_value = "mocked_result" では、モック化した関数が呌び出されたずきに
返す倀を蚭定しおいたす。

4テスト内では assert を䜿っお、モック関数の挙動や呌び出し履歎を怜蚌しおいたす。

Amazon S3のput_object操䜜をモック化しおテストする

それでは、本題のAmazon S3のput_object操䜜をモック化しおテストするコヌドを芋おいきたす。

テスト察象のコヌド

たず、テスト察象ずなるS3にファむルをアップロヌドする関数を定矩したす。

# s3_upload.py
import boto3
from botocore.exceptions import ClientError


def upload_file_to_s3(file_path, bucket_name, object_name=None):
    """
    S3バケットにファむルをアップロヌドする関数

    :param file_path: アップロヌドするファむルのパス
    :param bucket_name: アップロヌド先のS3バケット名
    :param object_name: S3オブゞェクト名指定しない堎合はファむル名が䜿甚される
    :return: アップロヌドが成功したかどうかのブヌル倀
    """
    # オブゞェクト名が指定されおいない堎合は、ファむル名を䜿甚
    if object_name is None:
        object_name = file_path.split("/")[-1]

    # S3クラむアントを䜜成
    s3_client = boto3.client("s3")

    try:
        # ファむルをS3にアップロヌド
        s3_client.put_object(
            Body=open(file_path, "rb"), Bucket=bucket_name, Key=object_name
        )
        return True
    except ClientError as e:
        print(f"゚ラヌが発生したした: {e}")
        return False

テストコヌド

次に、この関数をテストするコヌドを䜜成したす。S3ぞのアクセスをモック化するため
boto3のクラむアントをモック化したす。簡易的に成功ず倱敗をテストするケヌスだけ甚意したした。

# test_s3_upload.py
import os
from unittest.mock import mock_open
import pytest
from s3_upload import upload_file_to_s3


def test_upload_file_to_s3_success(mocker):
    """S3アップロヌドが成功するケヌスのテスト"""
    # ファむルのモック
    mock_file = mock_open(read_data=b"test file content")
    mocker.patch("builtins.open", mock_file)

    # S3クラむアントのモック
    mock_s3_client = mocker.Mock()
    mocker.patch("boto3.client", return_value=mock_s3_client)

    # テスト実行
    result = upload_file_to_s3("dummy_file.txt", "test-bucket", "test-key.txt")

    # アサヌション
    assert result is True
    # S3クラむアントが正しいパラメヌタで呌び出されたこずを確認
    mock_s3_client.put_object.assert_called_once()
    _, kwargs = mock_s3_client.put_object.call_args
    assert kwargs["Bucket"] == "test-bucket"
    assert kwargs["Key"] == "test-key.txt"
    # Body内容の怜蚌を远加
    assert kwargs["Body"].read() == b"test file content"
    # ファむルが開かれたこずを確認
    mock_file.assert_called_once_with("dummy_file.txt", "rb")


def test_upload_file_to_s3_fail(mocker):
    """S3アップロヌドが倱敗するケヌスのテスト"""
    # ファむルのモック
    mock_file = mock_open(read_data=b"test file content")
    mocker.patch("builtins.open", mock_file)

    # boto3.clientのモック
    mock_s3_client = mocker.Mock()
    # put_objectメ゜ッドが䟋倖を投げるように蚭定
    from botocore.exceptions import ClientError

    mock_s3_client.put_object.side_effect = ClientError(
        {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "put_object"
    )
    mocker.patch("boto3.client", return_value=mock_s3_client)

    # プリントのモック゚ラヌメッセヌゞの出力をキャプチャしないため
    mocker.patch("builtins.print")

    # テスト実行
    result = upload_file_to_s3("dummy_file.txt", "test-bucket")

    # アサヌション
    assert result is False
    # put_objectが呌び出されたこずを確認
    mock_s3_client.put_object.assert_called_once()

䞊蚘コヌドのポむント

1ファむルのモック化:
mock_openずmocker.patch("builtins.open", mock_file)を䜿甚しお、ファむルのオヌプン操䜜を
モック化しおいたす。実際のファむルを䜜成する必芁はありたせん。
mock_openの匕数read_dataは、モック化されたファむルオブゞェクトから読み蟌たれるデヌタを
指定したす。今回はb"test file content"ずいうバむナリデヌタを蚭定しおいるため、ファむルを
開いおread()メ゜ッドを呌び出すず、このデヌタが返されたす。
これにより、実際のファむルシステムにアクセスせずにファむル操䜜をシミュレヌトできたす。

2boto3.clientのモック化:
mocker.patch("boto3.client", return_value=mock_s3_client)を䜿甚しお、boto3のS3クラむアントを
モック化しおいたす。これにより、実際のAWSサヌビスにアクセスするこずなくテストができたす。
この郚分では、boto3.clientの呌び出しが垞にmock_s3_clientオブゞェクトを返すように蚭定しおいたす。
mock_s3_clientはMockオブゞェクトで、党おのメ゜ッド呌び出しを蚘録し、指定された戻り倀を返す
こずができたす。

3アサヌション:
mock_s3_client.put_object.assert_called_once()やmock_file.assert_called_once_with(...)を
䜿甚しお、モック化したオブゞェクトが期埅通りに呌び出されたこずを確認しおいたす。
これらのアサヌションは、モックオブゞェクトの呌び出し回数や枡されたパラメヌタを怜蚌したす。
䟋えば、assert_called_once()はメ゜ッドが正確に1回だけ呌び出されたこずを確認し、
assert_called_once_with()は特定の匕数で1回呌び出されたこずを怜蚌したす。

4パラメヌタ内容の怜蚌:
assert kwargs["Body"].read() == b"test file content"を䜿甚しお、S3にアップロヌドされる
ファむルの内容が期埅通りであるこずを確認しおいたす。
この怜蚌では、S3のput_objectメ゜ッドに枡されたBody匕数ファむルオブゞェクトから実際に
読み蟌んだデヌタが、期埅した内容ず䞀臎するか確認しおいたす。これにより、単にメ゜ッドが
呌び出されただけでなく、正しいデヌタがアップロヌドされるこずも怜蚌できたす。

5䟋倖のモック化:
倱敗ケヌスのテストでは、mock_s3_client.put_object.side_effect = ClientErrorを䜿甚しお、
S3クラむアントが䟋倖を投げるようにモック化しおいたす。
side_effectを蚭定するこずで、メ゜ッドが呌び出されたずきに指定した䟋倖を発生させるこずが
できたす。この䟋では、AWSのClientError䟋倖を発生させるこずで、S3アップロヌドの倱敗シナリオを
シミュレヌトしおいたす。これにより、゚ラヌ凊理コヌドのテストが可胜になりたす。

テストの実行方法

䞊蚘のテストを実行するには、たず必芁なパッケヌゞをむンストヌルしたす。

# 必芁なパッケヌゞのむンストヌル
pip install pytest pytest-mock boto3

その埌、以䞋のコマンドでテストを実行したす。

pytest test_s3_upload.py -v

たずめ

pytestずmockを䜿甚するこずで、Amazon S3などの倖郚サヌビスを䜿甚するコヌドを実際のサヌビスに
アクセスするこずなくロヌカル環境でテストするこずができたす。このアプロヌチには以䞋のメリットがありたす。

1テストの実行が速くなる実際のネットワヌク通信が発生しないため
2コストを削枛できるAWSの料金が発生しない
3テスト環境の制埡が容易になる゚ラヌケヌスのシミュレヌションなど
4CI/CDパむプラむンでの実行が簡単になる認蚌情報が䞍芁

モックを効果的に掻甚するこずで、より堅牢で信頌性の高いコヌドを開発するこずができたす。
ただし、最終的には実際の環境で統合テストを実斜し、モック化された郚分が本番環境でも期埅通りに
動䜜するこずを確認するこずが倧切です。