AWS Chalice で実装したアプリケーションのユニットテスト

# AWS Chalice とは

https://github.com/aws/chalice

AWS SDK for Python を開発するチームによってメンテナンスされている、AWS Lambda に展開するアプリケーションを実装するための Python のマイクロフレームワークです。
ビルトインされている `@api.route`, `@app.on_s3_event` などのデコレータを用いて関数を実装し、CLI で簡単にそれらを実際に AWS 上に展開することが出来るようになります。

例えば、chalice CLI の中にある `chalice new-project` コマンドでプロジェクトの雛形を作成するとこのような実装が生成されます。

```python
from chalice import Chalice

app = Chalice(app_name='test')


@app.route('/')
def index():
    return {'hello': 'world'}
```

`/` にアクセスしたら `{"hello": "world"}` が返ってきそうだということがひと目でわかりますよね。
では実際にこれを AWS 上で展開するためには、ですが、以下のコマンドを実行します。

```sh
$ chalice deploy
```

このコマンドが成功すると、AWS 上で Amazon API Gateway と連携する AWS Lambda が関連設定も自動で行われた状態でデプロイされます。つまり、この瞬間からこのエンドポイントにアクセスすることが出来るということです。

より詳細、より実践的な使用方法は公式ドキュメントや他のエントリに解説を譲りますが、**実装完了から最短で数十秒で**デプロイすることが適います。


# AWS Chalice で実装したアプリケーションのユニットテスト

さてここからが本題です。AWS Chalice で実装したアプリケーションのユニットテストはどのように書くことが出来るでしょうか？ Chalice のバージョン `1.12.0` 現在では、ビルトインでのテストスイートの実装は含まれていません。

このトピックは多くの Chalice ユーザーの関心事であり、これまでにもいくつかの議論が発生しています。
たとえばこのスレッドでは、Chalice に備わっているローカル起動のための実装をユニットテストに転用したらいいのではないかという意見がマジョリティとなっていました。
https://github.com/aws/chalice/issues/289

そして、このアプローチを `pytest` で利用出来るようにした `pytest-chalice` という実装があります。
https://pypi.org/project/pytest-chalice/

## pytest-chalice の使い方

ここでは `pytest` そのものについての解説は割愛します。必要に応じて別の場所でインプットしてください。

では実際にシンプルな API のための実装のユニットテストを記述してみましょう。まずは `pytest-chalice` をインストールしましょう。

```sh
$ pip install pytest-chalice
```

そして先述の例が再登場です。`/` にアクセスしたら `{"hello": "world"}` が返ってきそうだということがひと目でわかりますよね (2回目)。

```python
# project/app.py
from chalice import Chalice

app = Chalice(app_name='project')


@app.route('/')
def index():
    return {'hello': 'world'}
```

このアプリケーションのプロジェクトルートは `project/` としています。続いては同じディレクトリ配下に以下のレイアウトで `tests` ディレクトリと各ファイルを設置します。

```sh
$ tree -C tests
tests/
├── __init__.py
├── conftest.py
└── test_app.py

0 directories, 3 files
```

まずは `conftest.py` を以下のように実装します。`app` という名前で、実際にテストを行いたい対象の Chalice アプリケーションオブジェクトを返す fixture を設けることが非常に重要なポイントです。`pytest-chalice` はこの fixture から返却されるオブジェクトを用いて内部的にローカル起動し、そこにアクセスするためのテストクライアントオブジェクトを新たに fixture として登録しています。

```python
# project/tests/conftest.py
import pytest

from app import app as chalice_app


@pytest.fixture
def app():
    return chalice_app
```

続いて実際に `app.py` に対してのテストを実装してみましょう。`/` にアクセスしたら `{"hello": "world"}` が返って来そうな実装(3回目)なので、以下の挙動を確認する内容で記述してみます。

- `/` にアクセスしたら `{"hello": "world"}` が返ってくる
- レスポンスステータスが `200 OK`

```python
# project/tests/test_app.py
from http import HTTPStatus


def test_index(client):
    response = client.get('/')
    assert response.status_code == HTTPStatus.OK
    assert response.json == {'hello': 'world'}
```

`test_index` で受け取っている `client` が、先述した `pytest-chalice` が生成しているテストクライアントオブジェクトです。`app` fixture を設けていないと正常に動作しません。

さてここまででシンプルなアプリケーションに対してシンプルなユニットテストの実装が完了しました。実行してみましょう。

```sh
===================== test session starts =====================
platform darwin -- Python 3.7.4, pytest-5.2.2, py-1.8.0, pluggy-0.13.0
cachedir: .pytest_cache
rootdir: /private/tmp/project
plugins: chalice-0.0.4, env-0.6.2
collected 1 item

tests/test_app.py::test_index PASSED                    [100%]

====================== 1 passed in 0.01s ======================
```

以上です。類似の Example はこちらにありますのでスッと試したいという場合はご利用ください。
https://github.com/studio3104/pytest-chalice/tree/master/examples/simple

## pytest-chalice における注意点 (2019/12/08 現在)

- 3rd party project である
- Beta Stage である

ということに注意してください。現在コア機能を Chalice 本体に取り込むための PR が進行していますので、どのように取り込まれるかによっては将来のリリースでインターフェースなどが変更になる可能性があります。使い方がガラッと変わってしまうほどでにはならないはずですが、たとえば真面目に Type Hint を書いている場合はクラス名など名前空間が変わることで影響を受けるかもしれません。

- `@app.route` 以外の関数のテストを書きたい場合

現状では `@app.on_s3_event`, `@app.on_sqs_message` などで修飾されている関数のための特別な実装は用意がありません。もしこれらのテストを行いたい場合は、これらの event context を表現する fixture を用意して、それを用いるようにテストを実装するアプローチになると思います。