
pytest+motoでS3へのバケット一覧リクエストからオブジェクト確認リクエストまで細かくテストしてみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
Python製のロジックに対するユニットテストにはpytestは有効な一打です。そして、AWSサービスへアクセスするロジックに対するテストにはmotoという選択肢があります。
一例として、S3のファイル有無確認用ロジックのテストコードについて、motoを使って試行錯誤した末のまとめを書いてみました。
テストを組み立てる順番
今回は以下の順番で進めます。
- テスト環境の作成
- テストケースの追加
- クラス実装
- テストの実行
前提環境
- Python3.7
依存ライブラリ
- pipenv
- moto
- boto3
- pytest
テスト環境の作成
pipenv install --python 3.7 pipenv install boto3 pipenv install --dev moto pytest
ファイル・ディレクトリ構成
ユニットテスト実行を目的として、以下の構成にしました。各__init__.pyは空のファイルです。
setup.cfg
src/
  mypkg/
    __init__.py
    s3_access.py
tests/
  __init__.py
  foo/
    __init__.py
    conftest.py
    test_s3_access.py
setup.cfgの作成
pytestのコマンドだけで引数レス実行できるように指定します。
- testpaths
- テスト対象ディレクトリ
- python_files
- 対象ファイルパターン
- python_functions
- 対象テスト名パターン(指定されたパターンから始まるテストのみ
[tool:pytest] testpaths = tests/ python_files = test_*.py python_functions = test minversion = 3.7 addpot = --pdb
tests/foo/conftest.pyの作成
テストケースから実装クラス呼び出しを行うために、src/ディレクトリへのパスを追加します。
from pathlib import Path
import sys
sys.path.append(str(Path("./src/").resolve()))
テストケースの追加
pytestとmotoを併用して実装します。
motoを利用したテストは、必要に応じてテストケース用データを作成する必要があります。S3の場合、データの作成は@mock_s3デコレータを利用した関数内で、S3へのリクエストコードを追加・実行することによって行います。このデータは一時的なものであるため、必要に応じてテスト毎に作成します。
例として、bucketの作成前後を確認した手続きは以下のようになります。2回目のlist_buckets()にて、テストケース用データが空ではなくなったことを確認しています。
# S3上にbucketが作成されていないことを確認
% aws s3 ls | grep 'test_bucket'
% vim test_bucket.py
import boto3
from moto import mock_s3
@mock_s3
def test_s3_bucket():
    client = boto3.client('s3')
    assert client.list_buckets()['Buckets'] == []
    client.create_bucket(Bucket='test_bucket')
    assert client.list_buckets()['Buckets'] == []
    
% pytest test_bucket.py
..
..
========================== FAILURES ====================
_______________________ test_s3_bucket ________________
..
..
    @mock_s3
    def test_s3_bucket():
        client = boto3.client('s3')
        assert client.list_buckets()['Buckets'] == []
        client.create_bucket(Bucket='test_bucket')
>       assert client.list_buckets()['Buckets'] == []
E       AssertionError: assert [{'CreationDa...test_bucket'}] == []
E         Left contains one more item: {'CreationDate': datetime.datetime(2006, 2, 3, 16, 45, 9, tzinfo=tzutc()), 'Name': 'test_bucket'}
E         Use -v to get the full diff
# 実際にはS3上でbucketが作成されていないことを確認
% aws s3 ls | grep 'test_bucket'
 
ファイル有無確認用のテストコードを作成する
気をつけるべき点として、実際にはアップロードされないとしても、upload_fileで対象にするファイルは必ずローカルに存在しなければなりません。
% touch /path/to/file
tests/foo/test_s3_access.pyの作成
from moto import mock_s3
import boto3
import os
import pytest
import .mypkg.s3_access import S3Download
@mock_s3
def test_download():
    BUCKET = 'bucket'
    FILE_PATH = '/path/to/file'
    FILE_NAME = 'file'
    client = boto3.client('s3')
    client.create_bucket(Bucket=BUCKET)
    client.upload_file(FILE_PATH, BUCKET, FILE_NAME)
    s3_downloader = S3Download()
    assert s3_downloader.isexists(FILE_NAME) is True
クラスの実装
S3上のオブジェクト確認にはhead_objectを利用しました。
S3 — Boto 3 Docs 1.9.170 documentation
src/mypkg/s3_access.pyの作成
import boto3
from botocore.errorfactory import ClientError
    
class S3Download:
    def isexists(self, file_name):
        try:
            boto3.client('s3').head_object(Bucket='bucket', Key=file_name)
        except ClientError:
            return False
        return True
テストの実行
今回pytestのみで実行可能にしているため、引数は不要です。
% pytest ============ test session starts ================ .. .. collected 1 item test_bucket.py .
まとめ
「AWS上のサービスを操作するロジックのテストコードは書き辛い」と思った時こそ、motoの利用をおすすめします。










