この記事は公開されてから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の利用をおすすめします。