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なども同様の方法でテストできると思います。

以上。