プロジェクトのAPIのテストを自動化したら幸せになれた

## テストしてますか？

もちろんチームで開発するようなちゃんとしたコードではテストも書いていました
でも、自分の個人開発のコードにはテストの概念がありませんでした

自分の個人開発を続けるためのモチベーションとして「Small Step」を大切にしています
いきなり実装に数日かかるような大きな機能を作ろうとしてもたいていの場合は途中で挫折します
挫折すると忙しかったとかの理由を付けて実装から離れはじめ、最終的には...

少しずつ作っていくことのメリットとして１日にかける限られたコード量でも仕様通りに動いたときの達成感を得られます
しかし、裏を返せば完成した機能に満足して十分にテストしないまま「この機能は昨日動いたから大丈夫」と次の機能に進んでいます

こんなことを積み上げると、１日２日で作れるような簡単なアプリなら問題ないですがそれが工数の多いアプリとなると最終的に予期していない動作をし始めたり最悪の場合バグとバグが重なって正常に動作してしまったりなんて怖いことが起きます...

## いい加減テストを導入しよう

テストを導入しなかった一番の理由は「めんどくさかったから」です
コードは動いてるわけだし、テストコードを書く時間がまた増えるわけでめんどくさがりの自分は敬遠してました
もはや、テストせずに最後に結合テストで出たエラーを修正すればいいやなんて思ってた時期もありました

けれど、毎日ネットサーフィンをしているとQiitaを始めたくさんのテストの話やテスト自動化の話を見るわけです
さすがにそろそろ逃げられないと思い、重い腰を上げてテストを導入することにしました

## テストを導入するプロジェクト

個人開発のプロジェクトの技術スタックは基本的にNuxt×FastAPIで作っています
そこで今回は、よく名前を見るPythonのテストライブラリ「pytest」をFastAPIのベックエンドに導入してみました

### APIコードの例

```python
@app.post("/cardset/create")
def cardset_create(meta: dict, coursies: list, questions: list, siid: str = Depends(verify_token)):
    # REQ AUTH
    meta["author"] = siid
    cds = create_cardset(meta)
    for course in coursies:
        course["cardsetId"] = cds.cardsetId
        create_course(course)
    for question in questions:
        if question.get("questionId"):
            create_question_group(
                {"cardsetId": cds.cardsetId, "questionId": question["questionId"]})
        else:
            q = create_question({
                "question": question["question"],
                "answer": question["answer"],
                "approval": True
            })
    return cds.cardsetId
```
今回、導入するコードはクライアントからいろいろなデータを受け取りカードセットを作成するプログラムです
パラメータの数が多いからこそ、変なリクエストが混ざっていたり、必要なデータがなかったりといったテストケースが考えられます

ここではとにかくpytestを使ってみたかったので異常系のテストは実装せず、正常系のテストのみ作ってみました

```python
def test_cardset_create():
    # テストデータを作成してリクエストに渡す
    meta = {
        "name": "Test Cardset",
        "level": "Intermediate",
        "unit": "Unit 1"
    }
    coursies = [
        {
            "title": "Course 1",
            "subtitle": "Subtitle 1",
            "questions": 5,
            "notGoodOnly": False,
            "timelimit": 60,
            "wrongcount": 3
        }
    ]
    questions = [
        {
            "question": "What is 2 + 2?",
            "answer": "4"
        }
    ]
    response = client.post(
        "/cardset/create", json={"meta": meta, "coursies": coursies, "questions": questions})
    assert response.status_code == 200
    created_cardset = response.json()
    assert type(created_cardset) == int  # カードセットが正常に作成されたことを確認
```

このテストケースでは、以下の二点について確認しています
- リクエストに対するステータスコードが200であること
- 返ってきたデータが数値形式であること

## テストしてみる

上のテストパターンの他、いくつかのエンドポイントについて異常系と正常系のテストパターンを作成しました
テストパターンの作成にかなり時間がかかりました
もし、テストで思ったような結果が得られなければ今後個人開発でテストの実装なんてしたくないなんて思いながらただひたすらテストコードを書きました

pytestの実行は以下のコマンドで簡単に実行できます
```
# ディレクトリ
- src
    - api1.py
    - test_api1.py
    - api2.py
    - test_api2.py
```

```bash
src > pytest
```

これでsrcディレクトリにあるテストファイル`test_api1.py`と`test_api2.py`に記述したテストの内容が実行されます
ファイル名に関しては必ず`test_(被テストファイル名).py`とする必要があります

### いざテスト

```
============================================================================================= test session starts ==============================================================================================
platform win32 -- Python 3.10.10, pytest-7.4.3, pluggy-1.3.0
rootdir: 
plugins: anyio-3.6.2
collected 49 items

test_api1.py F.FF......FFFF...........
test_api2.py ..................F...F.

======================================================================================== 9 failed, 40 passed in 56.92s ========================================================================================= 
```

思ったよりエラーが多かったです
このまま実装をすすめていたらどうなっていたことやら...


## テスト周りの環境は整えよう...

テストを導入してから機能ごとの開発時間は伸びましたがそのあとの工程での原因不明のバグの発生率や要件定義漏れは格段に減りました

pytestのありがたいところは勝手にカレントディレクトリのテストファイルから全部テストしてくれるのでテスト実行して紅茶でも飲んで帰ってくるとテストが終わった状態のテスト失敗で染まった真っ赤なディスプレイがお出迎えしてくれます

テストが全部緑の「passed」になったときの爽快感は半端ないです

これを機にnuxtとかほかの部分のテストも導入していきたいです
(できれば今年中に触れておきたい...!)


ここまで読んでいただきありがとうございました！
良きテストライフを！

(もし、来年のアドカレでnuxtとかのテストの話を書いていたら「あ、こいつ１年間さぼったんだな」と思ってください)