AWS LambdaをChaliceとhatchとpytestでテストする
この記事はAWS Lambdaの品質を高めるために、pytestでテストを実施し、Chaliceとhatchでまとめていきます。
Chaliceはflask APIを描くノリでLambdaを書いて、デプロイすることができるライブラリです。
hatchはpyproject.tomlを使ったPythonプロジェクト管理を担います。
hatchにより Python UnitTestを使っても、pytestを使っても、hatch test
のコマンドでテストを実行することができたり、hatch run ***
で任意のコマンドを実行することができるようになるので、Pythonプロジェクトの暗黙知削減にも貢献できます。
0. 環境構築
今回はPython3.12を使っていきます。
$ python --version
Python 3.12.3
まずプロジェクトを作成するためにChaliceとhatchをインストールします。
- pipコマンドでhatchとchaliceをインストールしますcommand
$ pip install hatch chalice
Collecting hatch Using cached hatch-1.12.0-py3-none-any.whl.metadata (5.5 kB) Collecting chalice Using cached chalice-1.31.2-py3-none-any.whl.metadata (8.9 kB) Collecting click>=8.0.6 (from hatch) 〜〜〜(中略)〜〜〜 Successfully installed anyio-4.4.0 blessed-1.20.0 botocore-1.34.149 certifi-2024.7.4 chalice-1.31.2 click-8.1.7 distlib-0.3.8 filelock-3.15.4 h11-0.14.0 hatch-1.12.0 hatchling-1.25.0 httpcore-1.0.5 httpx-0.27.0 hyperlink-21.0.0 idna-3.7 inquirer-2.10.1 jaraco.classes-3.4.0 jaraco.context-5.3.0 jaraco.functools-4.0.1 jmespath-1.0.1 keyring-25.2.1 markdown-it-py-3.0.0 mdurl-0.1.2 more-itertools-10.3.0 packaging-24.1 pathspec-0.12.1 pexpect-4.9.0 pip-24.0 platformdirs-4.2.2 pluggy-1.5.0 ptyprocess-0.7.0 pygments-2.18.0 python-dateutil-2.9.0.post0 python-editor-1.0.4 pyyaml-6.0.1 readchar-4.1.0 rich-13.7.1 setuptools-72.1.0 shellingham-1.5.4 six-1.16.0 sniffio-1.3.1 tomli-w-1.0.0 tomlkit-0.13.0 trove-classifiers-2024.7.2 urllib3-2.2.2 userpath-1.9.2 uv-0.2.30 virtualenv-20.26.3 wcwidth-0.2.13 wheel-0.43.0 zstandard-0.23.0
1. プロジェクトの基礎を作成する
1.1. Chaliceとhatchの混成プロジェクトを作成
次にChaliceとhatchの混成プロジェクトを作成します。
ChaliceはAWS Lambdaを作るためのPython環境を提供し、hatchはテストやデプロイに関するコマンドや設定を一元的に管理するために使用します。
-
chaliceコマンドでchaliceプロジェクトを作成します
command$ chalice new-project sandbox-chalice-hatch
Your project has been generated in ./sandbox-chalice-hatch
-
作成したプロジェクト内に移動します
command$ cd sandbox-chalice-hatch
-
hatchコマンドを使って
pyproject.toml
を生成しますcommand$ hatch new sandbox-chalice-hatch --init
Description []:
Returnキーを入力
Wrote: pyproject.toml
1.3. 初期状態を記録する
Gitを使ってここまで生成したものを初期状態として記録しておきます。
-
gitのローカルリポジトリを作成します
command$ git init .
Initialized empty Git repository in .git/
-
今のフォルダ構造をステージに追加します
command$ git add .
-
変更内容をコミットします
command$ git commit -m "first commit."
[main (root-commit) fa4f9b2] first commit. 5 files changed, 101 insertions(+) create mode 100644 .chalice/config.json create mode 100644 .gitignore create mode 100644 app.py create mode 100644 pyproject.toml create mode 100644 requirements.txt
2. Chaliceのローカル実行環境を設定する
hatchからChaliceのローカル環境を実行してみます。
2.1. ローカル環境でアプリケーションを実行する
まずはシンプルにChaliceをローカル環境で実行してみます。
-
.chalice/config.json
を開いて、下記のように更新します.chalice/config.json{ "version": "2.0", "app_name": "sandbox-chalice-hatch", "stages": { "dev": { "api_gateway_stage": "api" + }, + "local": { + "environment_variables": { + "IS_LOCAL": "true" + } } } }
-
chalice localコマンドでローカル環境を起動します
command$ chalice local --stage local --port 8080
Restarting local dev server. Serving on http://127.0.0.1:8080
-
8080ポートで起動したようなので、curlコマンドでAPIにアクセスします
command$ curl "http://127.0.0.1:8080"
{"hello":"world"}
デフォルトで作成されたAPIレスポンスが返ってきました。
確認が終わったら、 Ctrl
+c
コマンドでアプリケーションを停止します
2.2. hatch経由でローカル環境のアプリケーションを実行する
次はhatch経由で実行してみます。
hatch経由にすることで、hatch env show
でpyprojectに含まれている環境やコマンドを一覧で確認できるので、しばらくしてから『このプロジェクトってどうやって実行するんだっけ?』だったり、『どうやってテストするんだっけ?』という悩みがなくなります。加えて環境ごとの依存ライブラリをpyproject.tomlで管理できるようになるので、requirements.txt
には実行に必要なもののみまとめることができ、実行環境の軽量化にもつながります。
-
pyproject.toml
のを編集してPythonバージョンと実行コマンドを記述するpyproject.toml[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "sandbox-chalice-hatch" dynamic = ["version"] description = '' readme = "README.md" - requires-python = ">=3.8" + requires-python = ">=3.12" license = "MIT" keywords = [] authors = [ { name = "********", email = "********" }, ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] - dependencies = [] + dependencies = [ + 'chalice' + ] [project.urls] 〜〜〜〜〜(中略)〜〜〜〜〜 [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] + [tool.hatch.envs.default.scripts] + chalice-local = "chalice local --stage local --port 8080"
-
記述した実行コマンドが認識されていることを確認する
command$ hatch env show
Standalone ┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Name ┃ Type ┃ Dependencies ┃ Scripts ┃ ┡━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ default │ virtual │ │ chalice-local │ ├─────────┼─────────┼──────────────┼───────────────┤ │ types │ virtual │ mypy>=1.0.0 │ chalice-local │ │ │ │ │ check │ └─────────┴─────────┴──────────────┴───────────────┘
-
hatch経由でchaliceコマンドを実行する
command$ hatch run chalice-local
Serving on http://127.0.0.1:8080
-
前回同様に8080ポートで起動したようなので、curlコマンドでAPIにアクセスします
command$ curl "http://127.0.0.1:8080"
{"hello":"world"}
確認が終わったら、 Ctrl
+c
コマンドでアプリケーションを停止します
2.3. 初期状態を記録する
Gitを使ってここまでの状況を記録しておきます。
-
初期状態からここまでの差分を確認します
command$ git diff
diff --git a/.chalice/config.json b/.chalice/config.json index 95424f7..652b363 100644 --- a/.chalice/config.json +++ b/.chalice/config.json @@ -4,6 +4,11 @@ "stages": { "dev": { "api_gateway_stage": "api" + }, + "local": { + "environment_variables": { + "IS_LOCAL": "true" + } } } } diff --git a/pyproject.toml b/pyproject.toml index 25ab69d..da28f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "sandbox-chalice-hatch" dynamic = ["version"] description = '' readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.12" license = "MIT" keywords = [] authors = [ @@ -16,15 +16,13 @@ authors = [ classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = [] +dependencies = [ + 'chalice' +] [project.urls] Documentation = "https://github.com/********/sandbox-chalice-hatch#readme" @@ -59,3 +57,6 @@ exclude_lines = [ "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] + +[tool.hatch.envs.default.scripts] +chalice-local = "chalice local --stage local --port 8080"
-
意図した通りの変更になっていることを確認し、コミットしていきます
command$ git add .chalice/config.json pyproject.toml $ git commit -m "chaliceをローカルで実行するための設定を行い、hatchにchalice-localコマンドを追加し た"
[main ab3d4e7] chaliceをローカルで実行するための設定を行い、hatchにchalice-localコマンドを追加した 2 files changed, 12 insertions(+), 6 deletions(-)
ここまででchalice localと同様の実行をhatch run chalice-local
で実行できるようになりました。
3. pytestを使ったテスト環境を構築する
hatchにはデフォルトでhatch test
として実行されるコマンドが準備されており、それはpytestを使用してテストを実施します。
今回のChalice+hatchの混成プロジェクトでもhatch test
でテストが実行できるように設定を施していきます。
3.1 テストを実行するための準備をする
まずはテストを実行するための準備をしていきます。
-
テスト用のフォルダを作成し、
__init__.py
を配置しますcommand$ mkdir tests $ touch tests/__init__.py
-
サンプルのテストファイルを、下記の通りに作成します
tests/test_app.pydef test_index(): assert True == False
-
hatchからpytestを呼び、テストを実行します
command$ hatch test
============================= test session starts ============================== platform darwin -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: ./sandbox-chalice-hatch configfile: pyproject.toml plugins: rerunfailures-14.0, mock-3.14.0, xdist-3.6.1 collected 1 item tests/test_app.py [100%] =================================== FAILURES =================================== __________________________________ test_index __________________________________ def test_index(): > assert True == False E assert True == False tests/test_app.py:2: AssertionError =========================== short test summary info ============================ FAILED tests/test_app.py::test_index - assert True == False ============================== 1 failed in 0.05s ===============================
無事にtests/test_app.py
に記述したassert True == False
で引っかかり、テストが終了しました。
3.2. Chaliceアプリケーションをテストする
次に先ほどのcurl "http://127.0.0.1:8080"
を実行したときに、{"hello":"world"}
が返ってくることを確認するテストを記述します。
-
tests/test_app.py
を編集し、Chaliceのテストに対応させますtests/test_app.py+ from http import HTTPStatus + + from chalice.test import Client + + from app import app + + def test_index(): - assert True == False + with Client(app) as client: + response = client.http.get("/") + assert response.status_code == HTTPStatus.OK + assert response.json_body == {"hello": "world"}
-
再度テストを実行します
command$ hatch test
============================= test session starts ============================== platform darwin -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: ./sandbox-chalice-hatch configfile: pyproject.toml plugins: rerunfailures-14.0, mock-3.14.0, xdist-3.6.1 collected 1 item tests/test_app.py . [100%] ============================== 1 passed in 0.02s ===============================
これでhatch経由でpytestを実行することができました。
4. テストカバレッジを計測する
hatchはhatch test --cover
コマンドでテストカバレッジを出力できます。
そのためにpyproject.toml
を設定します。
4.1. pyproject.tomlを編集し、カバレッジ設定を施す
-
pyproject.toml
を開いて、下記のように編集しますpyproject.toml〜〜〜〜〜(中略)〜〜〜〜〜 [tool.coverage.run] - source_pkgs = ["sandbox_chalice_hatch", "tests"] + source_pkgs = ["app"] branch = true parallel = true omit = [ - "src/sandbox_chalice_hatch/__about__.py", + "__about__.py", ] [tool.coverage.paths] - sandbox_chalice_hatch = ["src/sandbox_chalice_hatch", "*/sandbox-chalice-hatch/src/sandbox_chalice_hatch"] + sandbox_chalice_hatch = ["app"] - tests = ["tests", "*/sandbox-chalice-hatch/tests"] + tests = ["tests"] [tool.coverage.report] exclude_lines = [ "no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:", ] [tool.hatch.envs.default.scripts] chalice-local = "chalice local --stage local --port 8080"
4.2. hatchコマンドでカバレッジレポートを出力する
-
hatch test --cover
を実行し、カバレッジレポートを出力しますcommand$ hatch test --cover
============================= test session starts ============================== platform darwin -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: ./sandbox-chalice-hatch configfile: pyproject.toml plugins: rerunfailures-14.0, mock-3.14.0, xdist-3.6.1 collected 1 item tests/test_app.py . [100%] ============================== 1 passed in 0.70s =============================== Combined data file .coverage.M1.local.3073.XXlWbIZx Name Stmts Miss Branch BrPart Cover ------------------------------------------ app.py 5 0 2 0 100% ------------------------------------------ TOTAL 5 0 2 0 100%
これでテストカバレッジを出力することができます。
4.3. 変更内容をコミットする
-
.gitignore
を編集し、コミットから除外するファイルを追加します.gitignore.chalice/deployments/ .chalice/venv/ + __pycache__ + .*_cache + .coverage*
-
意図した通りの変更になっていることを確認します
command$ git diff
diff --git a/.gitignore b/.gitignore index 3dd60a9..d608bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .chalice/deployments/ .chalice/venv/ +__pycache__ +.*_cache +.coverage*
-
変更内容をコミットします
command$ git add .gitignore tests/__init__.py tests/test_app.py $ git commit -m "app.pyのテストを作成"
[main 7158464] app.pyのテストを作成 3 files changed, 8 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_app.py
まとめ
今回はhatch+Chaliceの混成プロジェクト作成と、hatchを通したpytestによるテストおよびテストカバレッジの取得まで、実装しました。
AWS LambdaのようなFaaSは自動テストが実装しづらく、結果として複雑な処理なのに毎回手動でテストをする羽目になったり、バグが起きても問題が特定しづらくなってしまったりとカオスになりがちです。
hatchやChaliceを使うことで初期の実装コストは上がりますが、新たな開発メンバーを加える際や、しばらくしてからメンテをする必要が出てきた際には、これらは必ず強力なツールになって、あなたを助けてくれることになると思います。
次の記事ではhatch test
と同様に、hatchに準備されているLintチェックコマンドを適用するための設定を施していきます。
Discussion