思い付きでテストを増やしたら工数が増えてバグを出した件について

**エエーンッ！**

この記事は、テスト自動化導入の時の話 by T-DASH Advent Calendar 2024のための記事です。

# pytest、導入することになった

上司：「pytest、使ってみたくない？」
私：「いや、別に……」
上司：「今月の工数、余ってるよね？　何か調査しないといけないよね。
テスト自動化に興味があったし、pytestがちょうどよいと思うんだよね」
私：「急に興味が出てきました！」

というわけで、上司の要望で、実際のジョブにpytestを実装することになりました。

# 目標
Docker環境で稼働するFastAPIで、エンドポイントを一通り実行する。

# pytestとは
[pytest](https://docs.pytest.org/en/stable/)は、pythonのテスト用のフレームワークです。

# 実装
### pytestのパッケージを追加

`requirements.txt`にpytestを記述する。
### テストを記述

ファイル名は`test_sample.py`など、「test_」から始める。
test系のファイルは、まとめて`test`というようなフォルダに入れると便利。

非同期関数のテストは、以下のようにして行う。

```python
import pytest
import httpx
from fastapi import FastAPI
from httpx import ASGITransport

# FastAPI アプリケーションを例として使用
app = FastAPI()

# ダミーエンドポイント
@app.get("/version")
async def get_version():
    return {"version": "1.0.0"}
```

何かデータを読み込んでテストしたいような場合、fixtureを使用してテスト内で動的なデータを準備。

```python
@pytest.fixture
async def dynamic_resource():
    # 非同期リソースの初期化
    resource = {"key": "value", "counter": 42}
    yield resource  # テストで使用するデータを提供
    # クリーンアップ処理
    resource.clear()
```

httpx の非同期クライアントを使用して API をテスト。

```python
@pytest.mark.asyncio
async def test_api_version():
    async with httpx.AsyncClient(transport=ASGITransport(app), base_url="http://testserver") as client:
        response = await client.get("/version")
        assert response.status_code == 200
        assert response.json() == {"version": "1.0.0"}
```

先ほどのfixture で生成した変数を用いて、非同期の挙動をテスト。

```python
@pytest.mark.asyncio
async def test_dynamic_resource_usage(dynamic_resource):
    # 動的に生成されたリソースを使用
    assert dynamic_resource["key"] == "value"
    assert dynamic_resource["counter"] == 42

    # 非同期的なロジックの例
    dynamic_resource["counter"] += 1
    assert dynamic_resource["counter"] == 43
```

非同期 API と fixtureで、動的なデータを使用する場合。

```python
@pytest.fixture
async def api_client():
    async with httpx.AsyncClient(transport=ASGITransport(app), base_url="http://testserver") as client:
        yield client

@pytest.mark.asyncio
async def test_api_with_dynamic_resource(api_client, dynamic_resource):
    dynamic_resource["key"] = "new_value"
    response = await api_client.get("/version")
    assert response.status_code == 200
    assert response.json() == {"version": "1.0.0"}
    assert dynamic_resource["key"] == "new_value"
```

こんなノリです。
すべてのエンドポイントを用意しました。

### テストの実行

testフォルダに入れたら、パスがずれるので、
Dockerコンテナ内で

```
PYTHONPATH=. pytest -s
```

などで指定して起動するようにしました。
`-s`オプションをつけるとログが見れて便利です。

# 結果……。

実装は、なんとかできるものだったし、テストも走らせることができるのですが、自分がやっていたジョブには**このやり方は適合しませんでした。**

まず第一に、上司がデプロイの前日に本気を出してリファクタリングするタイプのエンジニアでした。
前日にノリノリでリファクタリングした結果、品質は向上し、速度が大幅に改善した半面、**実はテストが通らなくなりました**。根本的なつくりから変わってしまったからです。

また、テストについては私が要請に従って実装したものなので、上司は把握しておらず、「テストが通らない」という状態が残り、それにテストを合わせる必要がありました。デプロイの当日でした。

また、本番用のデータベースを作成するとお金がかかるので、テスト用にデータ件数を減らしたデータベースを構築するようにしたのですが、折り悪く、「いくつかデータべースがあった場合、最新のタイムスタンプを持っているものを採用する」という機構にしてしまったため、テスト用のデータベースが最新になり、うまくテストを通らないというバグを出してしまいました。

これについては後から、「テストで生成したデータベースは消す」というコードを書いて対処しましたが、その分の効果が得られたか、かなり謎です。これ以降のプロジェクトでも、pytestをあえて実装していません。
したほうがいいのはよさそうなのですが、試験的にAPIを作っては捨て、作っては捨てという開発サイクルにいる場合、テストを作って運用するのは強い意志が必要そうです。

テストを一緒に作成し、テストのほうもメンテナンスをしていくという強い意志のもとに使用されるべきものです。
試験用のエンドポイントを作っては使い捨てるタイプのジョブにはあまり適合しませんでした。あるいはpytestを使う側の練度が足りていませんでした。

理想的には、「これを機に、しっかりとしたテストを書くことになりました」で〆たいところではありますが、実際にはそうなっていません。テストの実装文化が根付くのには、もう一歩踏み込んだ何かが必要なようです。

テスト自動化への道は遠い……っ。