【FastAPI】APIキーを利用したPOSTリクエストのpytestテスト方法

# 概要
FastAPIアプリケーションの特定のエンドポイントの動作を検証するテストコードを実装しました。
本記事では、GETリクエスト、APIキーを使用したPOSTリクエストのテストケースを紹介します。

以前、Djangoで実装した際の記事も作成したので、Djangoで実装する方はこちらご参考ください。
[【Django】APIキーを利用したPOSTリクエストをテストするためのテストケースの作成方法](https://qiita.com/Ryo-0131/items/3a54fcfd4356219e77dd)

# サンプルコード解説
## GETリクエストのテスト

```main.py
from fastapi import FastAPI

app = FastAPI()
## 以下省略
```

```test_main.py
from fastapi.testclient import TestClient
from .main import app


client = TestClient(app)

def test_get_success():
    response = client.get("/api")
    assert response.status_code == 200 # ステータスコードが200 OKであることを確認
```

`main.py`と`test_main.py`が同ディレクトリにある前提です。
そうでない場合は、import文の`from .main import app`を変えてください。
これにより、`main.py`モジュールの`app`オブジェクトをインポートしています。

`client = TestClient(app)`は、FastAPIアプリケーションをテストするための`TestClient`オブジェクトを作成しています。このテストクライアントを利用して、`get`や`post`などのHTTPメソッドを送信します。

`test_get_success()`関数では、`client.get()`メソッドを使用して`/api`エンドポイントにGETリクエストを送信し、レスポンスのステータスコードが200であることを確認しています。
`test_`から始まる名前の関数を作成するのは、`pytest`の命名規則と同じですね。

実行時は`pytest`で可能です。
※`pytest`は事前にインストールしてください。`pytest`とはテスト関数を自動的に検出し、実行するためのテストランナーのことです

## POSTリクエストのテスト
次にリクエストヘッダーとリクエストボディを投げるパターンのテストコードを実装します。

```test_main.py
from fastapi.testclient import TestClient
from .main import app
import copy


client = TestClient(app)

def test_get_success():
    response = client.get("/api")
    assert response.status_code == 200


## 追加 ##

# テスト用のリクエストデータを作成
params = {
    "sample_id": "",
    "xyz_cd" : "",
    "information": {
        "name": "abcdef",
        "address": "12345"
        }
     }


def test_post_with_api_key():
    x_api_key = "SAMPLE_API_KEY"
    request_data = params.copy()

    response = client.post("/api", json=request_data, 
                           headers={"Content-Type": "application/json", "x-api-key": x_api_key})

    assert response.status_code == 200

def test_post_with_wrong_api_key():
    x_api_key = "WRONG_API_KEY"
    request_data = params.copy()

    response = client.post("/api", json=request_data, 
                           headers={"Content-Type": "application/json", "x-api-key": x_api_key})

    assert response.status_code == 401 
```


`test_post_with_api_key()`関数では`SAMPLE_API_KEY`という正しいAPIキーが入っているのでステータスコード200でテストに成功します。ちなみに、`json`引数を使用することで、`request_data`オブジェクトがJSON形式のデータとしてリクエストに含まれることが保証されます(ただし、JSONにシリアライズできる`dict`である必要あり)。JSONの代わりにフォームデータを送信する必要がある場合は、代わりに`data`パラメータを使用するとのこと。


`test_post_without_api_key()`関数は、`WRONG_API_KEY`という誤ったAPIキーを提供してPOSTリクエストを送信しているので、ステータスコード401（Unauthorized）でテストに成功します。

任意のエラーステータスとエラーメッセージを返すようにしている場合は都度ここを変える必要あります。

## エラーレスポンスでテストする場合
例えば、`status`で期待通りのレスポンスを返す場合は`0000`、期待通りのエラーレスポンスを返す場合は`2222`だとします。また、レスポンスのJSONは、`response`エントリの中に`status`があるとします。

レスポンス形式：

```
{'response': {'sample_id': 'xxx', 'status': '0000', 'msg': 'xxxxxx'}}
```


その場合、以下のようになります。

```test_main.py

def test_post_with_api_key():
    x_api_key = "SAMPLE_API_KEY"
    request_data = params.copy()

    response = client.post("/api", json=request_data, 
                           headers={"Content-Type": "application/json", "x-api-key": x_api_key})
    response_data = response.json()

    assert response.status_code == 200
    assert response_data["response"]["status"] == "0000"

def test_post_with_wrong_api_key():
    x_api_key = "WRONG_API_KEY"
    request_data = params.copy()

    response = client.post("/api", json=request_data, 
                           headers={"Content-Type": "application/json", "x-api-key": x_api_key})
    response_data = response.json()

    assert response.status_code == 200
    assert response_data["response"]["status"] == "2222"
```

## 補足1: `print`デバッグしたいときは？
レスポンスの中身がどうなっているか確認したい場合などありますよね。
しかし、通常の`print`文を使っても、`pytest`の実行時にはターミナルに出力されません。

そこで、`pytest -s`コマンドを実行することで、標準出力を有効にすることができます。
ただし、本番環境でテストを実行する場合は外した方が良いですね。

## 補足2:`content_type`引数について
ちなみに、Djangoの`TestCase`では、以下のように記載します。

```py
from django.test import TestCase

class ApiTestCase(TestCase):
    def test_post_sample_with_api_key(self):
        headers = {'HTTP_X_API_KEY': 'SAMPLE_API_KEY'}
        data = {'key': 'value'}
        response = self.client.post('/api/', data=data, follow=True, content_type='application/json', accept='application/json', **headers)

        self.assertEqual(response.status_code, 200)

    def test_sample_without_api_key(self):
        headers = {'HTTP_X_API_KEY': 'WRONG_API_KEY'}
        data = {'key': 'value'}
        response = self.client.post('/api/', data=data, follow=True, content_type='application/json', accept='application/json', **headers)

        self.assertEqual(response.status_code, 401)
```

`HTTPX`の使い方をベースとしているとのことだったのでこれと同じ形でいけると思ったら、`TestClient`オブジェクトでは、通常`content_type`引数を受け取るようなメソッドは提供されていない模様。以下のエラーが出ます。

```
TypeError: TestClient.request() got an unexpected keyword argument 'content_type'
```

`TestClient`メソッドを適切に利用してリクエストを送信する場合、`headers`引数を使用して、`Content-Type`ヘッダーを設定する必要があります。ちょっと躓いたのでメモ。

## 参考
[テスト - FastAPI](https://fastapi.tiangolo.com/ja/tutorial/testing/)