Open21

AWS Lambda(Python) テストコード

mocknmockn

ゴール

  • Lambda関数の単体テストコードを作成し実行できるようにする
  • Pythonで書く
mocknmockn

環境変数の設定方法

  1. クラスのコンストラクタ内でクラス変数として初期化
  • Lambda_handlerをクラスとして書き換える
  1. monkeypatch.setenv() / monkeypatch.delenv()
  2. pytest-env

Lambda関数がグローバル変数として環境変数を設定している場合

  • テストコード内で環境変数の設定を行ってからテスト対象関数を読み込むようにする
  • monkeypatch.setenv()は使用できない
def test_xxx(condition, expected):
    os.environ["LOG_LEVEL"] = "WARNING"
    assert xxx(condition) == expected
mocknmockn

テスト実行コマンド

https://dev.classmethod.jp/articles/pytest-tips-cmd-options/

対象を指定してテスト実行

pytest test.py #テストファイル単位
pytest test.py::test_func #テスト関数単位(テストクラスなし)
pytest test.py::test_class #テストクラス単位
pytest test.py::test_class::test_func #テストファイル - テストクラス - テスト関数単位

テスト結果詳細表示

pytest test.py -vv
mocknmockn

カバレッジ

https://qiita.com/kg1/items/e2fc65e4189faf50bfe6

  • 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
mocknmockn
mocknmockn
  • freezegunでタイムゾーン指定して、テスト対象プログラム中でさらにタイムゾーンを指定していると、実際の値が期待値とずれる恐れがあるので注意。
  • タイムゾーン指定をプログラムで行っている場合は、freezegunではタイムゾーンの指定をしないこと。また、期待値にはタイムゾーン(日本なら+9)の値を指定すること。
mocknmockn

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

https://stackoverflow.com/questions/45925531/is-it-possible-to-make-boto3-ignore-signature-expired-error

ChatGPT回答

これはAWS SDKを使用してAmazon Web Services (AWS) との通信を行っている際に発生するエラーです。エラーメッセージに含まれる情報を見る限り、AWSの署名が無効な状態であることを示しています。このエラーは主に、システムの時刻が正しく同期されていない場合に発生することがあります。

以下の手順で解消できるか試してみてください。

システム時刻の確認と同期:
まず、コンピュータのシステム時刻が正しく設定されていることを確認してください。AWSの署名は時刻に基づいて生成されるため、時刻がずれているとエラーが発生することがあります。

タイムゾーンの確認:
時刻だけでなく、正しいタイムゾーンが設定されていることも重要です。タイムゾーンが正しくない場合、時刻の同期がうまく行われていても問題が発生することがあります。

AWSクレデンシャルの確認:
AWS SDKを使用している場合、正しいAWSアクセスキーとシークレットアクセスキーが設定されていることを確認してください。誤ったクレデンシャル情報を使用している場合もエラーが発生する可能性があります。

リトライ:
時刻の同期やクレデンシャルの確認後にも問題が解決しない場合、一時的な通信エラーかもしれません。一度プログラムをリトライしてみてください。

ネットワークの問題の確認:
時々、ネットワークの問題が原因でエラーが発生することがあります。ネットワーク接続が正常であることを確認してください。

これらの手順を試しても問題が解決しない場合、エラーメッセージに示された情報を元に、AWSのサポートに問い合わせることを検討してみてください。

freezegunで日付時刻を固定したせいで、システム時刻とのずれが生じたことが原因か。

mocknmockn

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)

参考

https://docs.python.org/ja/3.10/library/distutils.html
https://pypi.org/project/pytest-freezer/

mocknmockn

共通処理

  • 環境変数、ロガーの設定はLambda関数全体で共有できるようにしたい
  • グローバル変数として定義すると、テストコードでテストできない(参照エラーになる)
  • Lambda関数をクラスで書き直し、コンストラクタで初期化するよう実装すればテスト可能
mocknmockn
mocknmockn

テストコード中でbotocoreを使用して、ログ出力のテストをすると、予期しないINFOログが出力されてテストが通らなくなった。
以下コードを追加するとログ出力されなくなった。

boto3.set_stream_logger(name='botocore.credentials', level=logging.ERROR)

https://github.com/boto/botocore/issues/1841

mocknmockn

テストコードを書く時の心構え

https://qiita.com/keitakn/items/1a43d53e9c3b422ec5ef

以下は私がテストコードを書く時に特に気をつけている点です。
大きく2点。
冪等性が担保されている事
何度実行しても同じ結果が得られる事
通ったり通らなかったりするテストが存在しない
テストケース同士が疎結合である事
(例)テストA、テストBの順番で実行しないと通らないようなテストケースは作らない

テストケースで気をつけるべき点に記載した内容をカバーする為に以下の流れでテストを作成します。
テストケース毎にテストに必要な事前データを準備する
これをFixtureと呼びます
APIの単位でテストクラスを作成しそこに必要なテストケースを実装していく

先程とは別に以下のようなバリデーション専用のテストクラスを作成します。
専用のクラスを作成する理由は2つあります。
1つ目は、バリデーションのテストはDBへの事前データの登録が必要ないので、それを行わない事で少しでもテストの実行速度を早める為、2つ目はバリデーションのテストはパラメータの送信パターンが多いのでテストクラスが肥大化する傾向にあるからです。

テストコードも商用コードの一部と考え、メンテナンス性を意識する事が大事だと考えます。
テストの実行は継続していく事が大事でメンテナンス性の低いテストコードを書くとテストを修正するのが嫌になってきます。そうするとそのうちメンテナンスされなくなっていくので、テストコードに投資した意味が失われてしまいます。
テストケースの名前で何をテストしたいのか想像が出来る事
読みやすいコードを意識する、テストケースで重要なのは何を期待値としているかという点
複数のテストケースで同じデータを使いまわさない(テストデータも修正対象になる事があり得ます)

  • PytestでもSeeder使いたい
mocknmockn

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)
mocknmockn

pytset.ini

  • プロジェクトルートに配置
  • Pythonの実行パスを指定
    • メイン関数から自作モジュールの読み込みができるようになる
  • テストの実行パスを指定
  • テスト時の環境変数の指定
    • テスト時の環境変数をメイン関数で利用できるようになる
.
├── src/
│   ├── lambda_function.py
│   └── sub.py
├── tetsts/
│   ├── test_lambda_function.py
│   └── test_sub.py
└── pytest.ini
pytest.ini
[pytest]
env = 
  LOG_LEVEL=DEBUG
pythonpath = src
tsetpaths = tests

参考

https://docs.pytest.org/en/stable/reference/customize.html