⚖️

AWS LambdaをChaliceとhatchとpytestでテストする

2024/08/28に公開

この記事はAWS Lambdaの品質を高めるために、pytestでテストを実施し、Chaliceとhatchでまとめていきます。

image

Chaliceはflask APIを描くノリでLambdaを書いて、デプロイすることができるライブラリです。
hatchはpyproject.tomlを使ったPythonプロジェクト管理を担います。
hatchにより Python UnitTestを使っても、pytestを使っても、hatch testのコマンドでテストを実行することができたり、hatch run *** で任意のコマンドを実行することができるようになるので、Pythonプロジェクトの暗黙知削減にも貢献できます。

0. 環境構築

今回はPython3.12を使っていきます。

command
$ python --version
Python 3.12.3

まずプロジェクトを作成するためにChaliceとhatchをインストールします。

  1. 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はテストやデプロイに関するコマンドや設定を一元的に管理するために使用します。

  1. chaliceコマンドでchaliceプロジェクトを作成します

    command
    $ chalice new-project sandbox-chalice-hatch
    
    Your project has been generated in ./sandbox-chalice-hatch
    
  2. 作成したプロジェクト内に移動します

    command
    $ cd sandbox-chalice-hatch
    
  3. hatchコマンドを使って pyproject.toml を生成します

    command
    $ hatch new sandbox-chalice-hatch --init
    
    Description []: 
    

    Returnキーを入力

    Wrote: pyproject.toml
    

1.3. 初期状態を記録する

Gitを使ってここまで生成したものを初期状態として記録しておきます。

  1. gitのローカルリポジトリを作成します

    command
    $ git init .
    
    Initialized empty Git repository in .git/
    
  2. 今のフォルダ構造をステージに追加します

    command
    $ git add .
    
  3. 変更内容をコミットします

    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をローカル環境で実行してみます。

  1. .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"
    +       }
          }
        }
      }
    
  2. chalice localコマンドでローカル環境を起動します

    command
    $ chalice local --stage local --port 8080
    
    Restarting local dev server.
    Serving on http://127.0.0.1:8080
    
  3. 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 には実行に必要なもののみまとめることができ、実行環境の軽量化にもつながります。

  1. 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"
      
    
  2. 記述した実行コマンドが認識されていることを確認する

    command
    $ hatch env show
    
    Standalone
    ┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
    ┃ Name    ┃ Type    ┃ Dependencies ┃ Scripts       ┃
    ┡━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
    │ default │ virtual │              │ chalice-local │
    ├─────────┼─────────┼──────────────┼───────────────┤
    │ types   │ virtual │ mypy>=1.0.0  │ chalice-local │
    │         │         │              │ check         │
    └─────────┴─────────┴──────────────┴───────────────┘
    
  3. hatch経由でchaliceコマンドを実行する

    command
    $ hatch run chalice-local
    
    Serving on http://127.0.0.1:8080
    
  4. 前回同様に8080ポートで起動したようなので、curlコマンドでAPIにアクセスします

    command
    $ curl "http://127.0.0.1:8080"
    
    {"hello":"world"}
    

確認が終わったら、 Ctrl+cコマンドでアプリケーションを停止します

2.3. 初期状態を記録する

Gitを使ってここまでの状況を記録しておきます。

  1. 初期状態からここまでの差分を確認します

    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"
    
  2. 意図した通りの変更になっていることを確認し、コミットしていきます

    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 テストを実行するための準備をする

まずはテストを実行するための準備をしていきます。

  1. テスト用のフォルダを作成し、 __init__.pyを配置します

    command
    $ mkdir tests
    $ touch tests/__init__.py
    
  2. サンプルのテストファイルを、下記の通りに作成します

    tests/test_app.py
    def test_index():
        assert True == False
    
  3. 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"}が返ってくることを確認するテストを記述します。

  1. 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"}
    
  2. 再度テストを実行します

    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を編集し、カバレッジ設定を施す

  1. 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コマンドでカバレッジレポートを出力する

  1. 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. 変更内容をコミットする

  1. .gitignoreを編集し、コミットから除外するファイルを追加します

    .gitignore
      .chalice/deployments/
      .chalice/venv/
    + __pycache__
    + .*_cache
    + .coverage*
    
  2. 意図した通りの変更になっていることを確認します

    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*
    
  3. 変更内容をコミットします

    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