resize-image-app-project プロジェクトで作成した AWS Lambda のユニットテストを作成する(local動作版)
概要
記事一覧はこちらです。
S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる で作成した AWS Lambda のユニットテストを作成してみます。
前回 deploy するための外部ライブラリを収集するのに lambci/lambda:build-python3.8 という Docker Image を利用したので、おそらく Docker 上で動作確認することもできると思いますが、Testing Serverless Services という Web ページを見つけて local で動作させることもできるようなので試してみます。
参照したサイト・書籍
Testing Serverless Services
https://towardsdatascience.com/testing-serverless-services-59c688812a0dPython実践入門 ── 言語の力を引き出し、開発効率を高める WEB+DB PRESS plus
- unittest モジュールの使い方が記載されていて参考にしました。
spulec / moto
https://github.com/spulec/motoWhat is the fastest way to empty s3 bucket using boto3?
https://stackoverflow.com/questions/43326493/what-is-the-fastest-way-to-empty-s3-bucket-using-boto3
目次
- ディレクトリ名を resize-service → resize_service に変更する
- プロジェクトのルートディレクトリ直下に tests ディレクトリを作成してテストのサンプルを作成・実行する
- moto モジュールをインストールする
- test_resize.py にテストを実装して実行してみる。。。が NoCredentialsError が発生する
- NoCredentialsError のエラーを解消する
- IntelliJ IDEA から debug 実行してみる
- 最後にディレクトリ構成を記載する
手順
ディレクトリ名を resize-service → resize_service に変更する
プロジェクト名に -
(ハイフン) が含まれているとテストのファイルから AWS Lambda のファイルを import できません。IDEA で import 文を書いた時に赤波線が表示されて気づきました。
PEP 8 -- Style Guide for Python Code の Package and Module Names には all-lowercase names
にするか Underscores
が使用できると記載されていますが、ディレクトリ名を _
(アンダースコア) に変更して serverless.yml の service 名も同じに変更すると今度は deploy が成功しません。
The stack service name "resize_service-dev" is not valid. A service name should only contain alphanumeric (case sensitive) and hyphens. It should start with an alphabetic character and shouldn't exceed 128 characters.
のエラーが出ます。
ディレクトリ名は _
(アンダースコア) に変更し(resize_service)、serverless.yml の service 名は -
(ハイフン)のままにします(resize-service)。
service: resize-service ..........
ちなみに npx sls create --template aws-python3 --path xxxxxxxx_service
と create コマンドに渡すディレクトリ名に _
(アンダースコア)を含めた場合、
serverless.yml の service 名は -
(ハイフン)になりました。
プロジェクトのルートディレクトリ直下に tests ディレクトリを作成してテストのサンプルを作成・実行する
プロジェクトのルートディレクトリ直下に tests ディレクトリを作成し、その下に __init__.py
というファイルを作成します(中身は空)。
test_resize.py というファイルも作成し以下の内容を記述してから、
import unittest class TestResizeService(unittest.TestCase): def test_resize(self): None
コマンドプロンプトを起動して以下のコマンドを実行すると、unittest モジュールにテストのファイルが認識されて1件成功します。
venv\Scripts\activate
python -m unittest -v
テストを実行すると tests ディレクトリの下に __pycache__
というディレクトリが作成されますが git で管理する必要のないディレクトリなので .gitignore に __pycache__/
の記述を追加して無視されるようにします。
.......... # Ignore Python unittest __pycache__/ ..........
ちなみに __init__.py
というファイルがないと unittest モジュールにテストが認識されません。最初テストのファイルだけ作成して認識されなくて少し悩みました。。。
moto モジュールをインストールする
AWS Service をモック化してくれる moto モジュールをインストールします。
pip install moto
を実行します。依存するモジュールがいろいろインストールされるので少し時間がかかります。
test_resize.py にテストを実装して実行してみる。。。が NoCredentialsError が発生する
tests ディレクトリの下にテストで使用する sample.jpg をコピーし、s3_event.json というファイルを新規作成して S3 のイベントの JSON を記述します。
{ "Records": [ { "eventVersion": "2.1", "eventSource": "aws:s3", "awsRegion": "ap-northeast-1", "eventTime": "2020-06-10T00:45:25.995Z", "eventName": "ObjectCreated:Put", "userIdentity": { "principalId": "AWS:XXXXXXXXXXXXXXXXXXXXX" }, "requestParameters": { "sourceIPAddress": "xxx.xxx.xxx.xxx" }, "responseElements": { "x-amz-request-id": "11F4BEB2A6E4D924", "x-amz-id-2": "k2RJ6yeU8sk5tRp7ntfyM8QDmR58rF1TIdacJimdExhLOOSQ3N6pOtIAWXyk6pGjTeBGOBx995ELqaK53zhdAo6Ky/PZb08e" }, "s3": { "s3SchemaVersion": "1.0", "configurationId": "66e09001-2a37-4900-9a98-06da126cc896", "bucket": { "name": "ksbysample-upload-bucket", "ownerIdentity": { "principalId": "XXXXXXXXXXXXXX" }, "arn": "arn:aws:s3:::ksbysample-upload-bucket" }, "object": { "key": "sample.jpg", "size": 669286, "eTag": "febe0aaf4d817457776b6be293973442", "sequencer": "005EE02D28714EE2DC" } } } ] }
test_resize.py にテストを実装します。
import json import unittest import boto3 from moto import mock_s3 from resize_service import handler @mock_s3 class TestResizeService(unittest.TestCase): UPLOAD_BUCKET = 'ksbysample-upload-bucket' RESIZE_BUCKET = 'ksbysample-resize-bucket' def setUp(self): s3_client = boto3.client('s3') s3_client.create_bucket(Bucket=TestResizeService.UPLOAD_BUCKET) s3_client.create_bucket(Bucket=TestResizeService.RESIZE_BUCKET) def tearDown(self): s3 = boto3.resource('s3') upload_bucket = s3.Bucket(TestResizeService.UPLOAD_BUCKET) upload_bucket.objects.all().delete() upload_bucket.delete() resize_bucket = s3.Bucket(TestResizeService.RESIZE_BUCKET) resize_bucket.objects.all().delete() resize_bucket.delete() def test_resize(self): s3_client = boto3.client('s3') s3_client.upload_file('tests/sample.jpg', TestResizeService.UPLOAD_BUCKET, 'sample.jpg') with open('tests/s3_event.json', 'r') as f: event = json.load(f) handler.resize(event, None) thumb_object = s3_client.get_object(Bucket=TestResizeService.RESIZE_BUCKET, Key='sample_thumb.jpg') self.assertEqual(thumb_object['ResponseMetadata']['HTTPStatusCode'], 200) self.assertGreater(int(thumb_object['ResponseMetadata']['HTTPHeaders']['content-length']), 0) # 生成されたサムネイル画像をダウンロードすることも出来る(実際に作成される) # s3_client.download_file(TestResizeService.RESIZE_BUCKET, 'sample_thumb.jpg', # 'tests/sample_thumb.jpg')
python -m unittest -v
を実行してみると raise NoCredentialsError
のエラーが出てテストが失敗しました。。。
NoCredentialsError のエラーを解消する
Web で調べると AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY が設定されていないことが原因と書かれている記事を見かけるのですが、解決したという人としなかったという人を見かける上に、テストの方はモックの S3 に問題なくアクセスできていて今ひとつ良く分かりません。
試しに AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY をいくつかの方法で設定してみると以下の結果でした。
- test_resize メソッド内で AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY の環境変数を設定するのは NG。
- resize_service/handler.py で
s3_client = boto3.client('s3')
→s3_client = boto3.client('s3', aws_access_key_id='test', aws_secret_access_key='test')
に変更すると OK。 python -m unittest -v
を実行するコマンドプロンプトで AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY の環境変数を設定すると OK。
何となく原因が分かりました。おそらく以下の動作になっているのでしょう。
- test_resize.py が読み込まれる。
from resize_service import handler
により resize_service/handler.py が読み込まれる。- resize_service/handler.py のグローバルで
s3_client = boto3.client('s3')
が実行される。この時は boto3 の s3 クライアントはまだモックではなく Credentials の情報がセットされない。 - test_resize.py の TestResizeService.test_resize メソッドが実行される。この時は mock_s3 デコレータ により boto3 の s3 クライアントはモック化されて Credentials の情報がセットされる。
- 結果として test_resize.py 内の処理ではモックの S3 に問題なくアクセスできるが、resize_service/handler.py 内の処理でモックの S3 にアクセスしようとすると
raise NoCredentialsError
のエラーが出る。
resize_service/handler.py の s3_client = boto3.client('s3')
を記述する位置を resize 関数の最初に変更します。
.......... logger = logging.getLogger() logger.setLevel(logging.INFO) def resize_image(image_path, resized_path): with Image.open(image_path) as image: image.thumbnail(thumbnail_size) image.save(resized_path) def resize(event, context): s3_client = boto3.client('s3') for record in event['Records']: bucket = record['s3']['bucket']['name'] ..........
python -m unittest -v
を実行すると今度は成功しました。
IntelliJ IDEA から debug 実行してみる
test_resize.py 内の handler.resize(event, None)
の行に breakpoint をセットしてから、IDEA のエディタの左側に表示されている矢印をクリックして Debug 'Unittests for test_r...'
を選択します。
初回は FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。: 'tests/sample.jpg'
のエラーが出てエラーになります。
IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「Python tests」の下に表示されている項目を選択してから画面右側の「Working directory」の設定を D:\project-serverless\ksbysample-serverless\resize-image-app-project\tests
→ D:\project-serverless\ksbysample-serverless\resize-image-app-project
に変更します(プロジェクトのルートディレクトリにする)。
再度 Debug 'Unittests for test_r...'
を選択して debug 実行すると今度は breakpoint で止まり、「Step Into」ボタンを押せば resize_service/handler.py の resize 関数に進みます。
最後にディレクトリ構成を記載する
Python には標準で unittest モジュールが用意されていたり、moto という AWS Service をモック化するモジュールが存在したり、IntelliJ IDEA で Lambda のソースを debug 実行できたりして、Serverless は debug しにくいと思っていましたが結構開発しやすくて意外でした。
履歴
2020/06/13
初版発行。