AWS Lambda(Python) テストコード
ゴール
- Lambda関数の単体テストコードを作成し実行できるようにする
- Pythonで書く
- Pytestが主流
- 標準ライブラリのunittesの上位互換
Pytest
pip install pytest pytest-mock
モック
汎用(pytest-mock)
AWSリソース特化(moto)
環境変数の設定方法
- クラスのコンストラクタ内でクラス変数として初期化
- Lambda_handlerをクラスとして書き換える
- monkeypatch.setenv() / monkeypatch.delenv()
- pytest-env
Lambda関数がグローバル変数として環境変数を設定している場合
- テストコード内で環境変数の設定を行ってからテスト対象関数を読み込むようにする
- monkeypatch.setenv()は使用できない
def test_xxx(condition, expected):
os.environ["LOG_LEVEL"] = "WARNING"
assert xxx(condition) == expected
テスト実行コマンド
対象を指定してテスト実行
pytest test.py #テストファイル単位
pytest test.py::test_func #テスト関数単位(テストクラスなし)
pytest test.py::test_class #テストクラス単位
pytest test.py::test_class::test_func #テストファイル - テストクラス - テスト関数単位
テスト結果詳細表示
pytest test.py -vv
カバレッジ
- pytest-cov
使い方
インストール
pip install pytest-cov
カバレッジ取得
pytest -v --cov=CODE_DIRECTORY
pytest test.py::test_class::test_func -v --cov=test.py
網羅されなかった行を表示
pytest -v --cov=CODE_DIRECTORY --cov-report=term-missing
pytest est.py::test_class::test_func -v --cov=test.py --cov-report=term-missing
カバレッジレポートをHTMLで出力
pytest -v --cov=CODE_DIRECTORY --cov-report=html
pytest -v --cov=CODE_DIRECTORY::test_func --cov-report=html
日付のテスト
pip install pytest-freezegun
- freezegunでタイムゾーン指定して、テスト対象プログラム中でさらにタイムゾーンを指定していると、実際の値が期待値とずれる恐れがあるので注意。
- タイムゾーン指定をプログラムで行っている場合は、freezegunではタイムゾーンの指定をしないこと。また、期待値にはタイムゾーン(日本なら+9)の値を指定すること。
freezegunで日付時刻を固定した状態で、JSTに変換した日付時刻のテストで以下エラーが発生。
An error occurred (InvalidSignatureException) when calling the PutItem operation: Signature not yet current: 20230810T123456Z is still later than 20230810T072338Z (20230810T070838Z + 15 min.)')] = <_pytest.logging.LogCaptureFixture object at 0x7f380928d840>.record_tuples
ChatGPT回答
これはAWS SDKを使用してAmazon Web Services (AWS) との通信を行っている際に発生するエラーです。エラーメッセージに含まれる情報を見る限り、AWSの署名が無効な状態であることを示しています。このエラーは主に、システムの時刻が正しく同期されていない場合に発生することがあります。
以下の手順で解消できるか試してみてください。
システム時刻の確認と同期:
まず、コンピュータのシステム時刻が正しく設定されていることを確認してください。AWSの署名は時刻に基づいて生成されるため、時刻がずれているとエラーが発生することがあります。
タイムゾーンの確認:
時刻だけでなく、正しいタイムゾーンが設定されていることも重要です。タイムゾーンが正しくない場合、時刻の同期がうまく行われていても問題が発生することがあります。
AWSクレデンシャルの確認:
AWS SDKを使用している場合、正しいAWSアクセスキーとシークレットアクセスキーが設定されていることを確認してください。誤ったクレデンシャル情報を使用している場合もエラーが発生する可能性があります。
リトライ:
時刻の同期やクレデンシャルの確認後にも問題が解決しない場合、一時的な通信エラーかもしれません。一度プログラムをリトライしてみてください。
ネットワークの問題の確認:
時々、ネットワークの問題が原因でエラーが発生することがあります。ネットワーク接続が正常であることを確認してください。
これらの手順を試しても問題が解決しない場合、エラーメッセージに示された情報を元に、AWSのサポートに問い合わせることを検討してみてください。
freezegunで日付時刻を固定したせいで、システム時刻とのずれが生じたことが原因か。
pytest-freezegunではなくpytest-freezerを使おう
概要
pytest-freezegunでは内部で使用されているdistutilsがpython3.10および3.11から非推奨となり、Warningが発生する。
代わりにpytest-freezerを使用するとWarningが解消する。
使い方
モジュールインストール
pip install pytest-freezer
コード
def test_freezer_methods(freezer):
freezer.move_to("2024-01-02 12:34:56")
assert datetime.now() == datetime(2024, 1, 2, 12, 34, 56)
参考
標準出力とログ出力のテスト
例外のテスト
モック
例外
SteamingBody
共通処理
- 環境変数、ロガーの設定はLambda関数全体で共有できるようにしたい
- グローバル変数として定義すると、テストコードでテストできない(参照エラーになる)
- Lambda関数をクラスで書き直し、コンストラクタで初期化するよう実装すればテスト可能
Boto3 (AWS Python SDK)
resourceの改修凍結
DynamoDB boto3.client の型変換
- boto3.clientでget_itemすると、レスポンスの各項目の値が型とマッピングされていて値を取得しずらい
- boto3標準のTypeSerializer, TypeDeserializerを使うと、楽に型変換ができる
テストコード中でbotocoreを使用して、ログ出力のテストをすると、予期しないINFOログが出力されてテストが通らなくなった。
以下コードを追加するとログ出力されなくなった。
boto3.set_stream_logger(name='botocore.credentials', level=logging.ERROR)
テスト実行時のログを表示する
python -m pytest --log-cli-level=DEBUG [テスト実行の対象ディレクトリ/ファイル]
テスト実行時の標準出力を表示する
python -m pytest -s [テスト実行の対象ディレクトリ/ファイル]
テストコードを書く時の心構え
以下は私がテストコードを書く時に特に気をつけている点です。
大きく2点。
冪等性が担保されている事
何度実行しても同じ結果が得られる事
通ったり通らなかったりするテストが存在しない
テストケース同士が疎結合である事
(例)テストA、テストBの順番で実行しないと通らないようなテストケースは作らない
テストケースで気をつけるべき点に記載した内容をカバーする為に以下の流れでテストを作成します。
テストケース毎にテストに必要な事前データを準備する
これをFixtureと呼びます
APIの単位でテストクラスを作成しそこに必要なテストケースを実装していく
先程とは別に以下のようなバリデーション専用のテストクラスを作成します。
専用のクラスを作成する理由は2つあります。
1つ目は、バリデーションのテストはDBへの事前データの登録が必要ないので、それを行わない事で少しでもテストの実行速度を早める為、2つ目はバリデーションのテストはパラメータの送信パターンが多いのでテストクラスが肥大化する傾向にあるからです。
テストコードも商用コードの一部と考え、メンテナンス性を意識する事が大事だと考えます。
テストの実行は継続していく事が大事でメンテナンス性の低いテストコードを書くとテストを修正するのが嫌になってきます。そうするとそのうちメンテナンスされなくなっていくので、テストコードに投資した意味が失われてしまいます。
テストケースの名前で何をテストしたいのか想像が出来る事
読みやすいコードを意識する、テストケースで重要なのは何を期待値としているかという点
複数のテストケースで同じデータを使いまわさない(テストデータも修正対象になる事があり得ます)
- PytestでもSeeder使いたい
テスト用DBを立てる方法
- アプリケーションのコードにテストのためのロジックを入れない
- PynamoDBのモデル定義をテスト用のテーブル作成に活用する
Stream
Json bodyをStreamに変換
import botocore
from io import BytesIO
def change_streaming_body(body_json):
# エンコード(encode()はデフォルトでutf-8。)
body_encoded = json.dumps(body_json).encode()
# body_encoded = "{\"aaa\": 3}".encode() # 勿論直接dumps後の文字列を埋めるのでもok
content_length = len(body_encoded)
# StreamingBodyへ整形する。
streaming_body = botocore.response.StreamingBody(
BytesIO(body_encoded), content_length
)
return streaming_body
streaming_body = change_streaming_body({"statusCode": 200, "body": "Email successfully sent."})
ストリーム用のログ設定
import boto3
boto3.set_stream_logger(name="botocore.credentials", level=logging.ERROR)
pytset.ini
- プロジェクトルートに配置
- Pythonの実行パスを指定
- メイン関数から自作モジュールの読み込みができるようになる
- テストの実行パスを指定
- テスト時の環境変数の指定
- テスト時の環境変数をメイン関数で利用できるようになる
.
├── src/
│ ├── lambda_function.py
│ └── sub.py
├── tetsts/
│ ├── test_lambda_function.py
│ └── test_sub.py
└── pytest.ini
[pytest]
env =
LOG_LEVEL=DEBUG
pythonpath = src
tsetpaths = tests
参考