【Day19】自動テストを実装する（全体の構想＆APIユニットテスト編）

## はじめに

勉強とアウトプットを兼ねて、Webアプリ開発の記録を記事にしてみます。

個人のメモ的な側面が強いです。
Web開発初心者すぎて至らぬところだらけだと思いますが、暖かい目で見ていただければ幸いです。

↓ これまでの内容 ↓
[【Day0】アプリが完成するまで毎日投稿](https://qiita.com/midorigame/items/f9b9a445c945b4a837b1)
[【Day1】開発環境を考える](https://qiita.com/midorigame/items/e26344599e2ffaa22033)
[【Day2】開発環境を構築する、、、はずでした](https://qiita.com/midorigame/items/3f57f2691a8e1074adb8)
[【Day3】まずはフロントだけ開発環境を構築する](https://qiita.com/midorigame/items/9de27ba72aaef504a833)
[【Day4】バックエンドの開発環境を整えつつ、マルチコンテナ対応のDevcontainerにする](https://qiita.com/midorigame/items/5b95374cd85f715a6498)
[【Day5】お試し実装！ファイルをアップロードしてバックエンドで処理する](https://qiita.com/midorigame/items/9663f0063b95b501c08a)
[【Day6】Docker Desktopが起動しなくなり泣きながら原因調査する](https://qiita.com/midorigame/items/d07df1caf05267d0609d)
[【Day7】画像にフレームとExif情報を付けてみる](https://qiita.com/midorigame/items/e2d85ce8dd7a5bcb5daf)
[【Day8】フロントに表示する画像のサイズを小さくする](https://qiita.com/midorigame/items/f7f5f9b14fb79259c716)
[【Day9】フロントエンドとバックエンドをそれぞれデプロイする](https://qiita.com/midorigame/items/4c8037024e667ff1105b)
[【Day10】結局コンテナ1つで開発する方が楽かもしれない](https://qiita.com/midorigame/items/60b484b2007c3aab474b)
[【Day11】アプリが一通り完成！！](https://qiita.com/midorigame/items/38ddccc09f0625aaebd6)
[【Day12】FastAPIのコードをリファクタリングする](https://qiita.com/midorigame/items/72ed91ff9add5504fab9)
[【Day13】機能実装！フレームに記載するExif情報を編集できるようにする（前編）](https://qiita.com/midorigame/items/790b0fcd54620f9fb573)
[【Day14】ReactのリファクタリングとUI変更を同時にやった](https://qiita.com/midorigame/items/b492ae717e59fb137b70)
[【Day15】機能実装！フレームに記載するExif情報を編集できるようにする（後編）](https://qiita.com/midorigame/items/9a649aa2a0a17d1db552)
[【Day16】インスタ用のキャプションを生成する（前編）](https://qiita.com/midorigame/items/4aa9bf4f1d05180436db)
[【Day17】インスタ用のキャプションを生成する（中編）](https://qiita.com/midorigame/items/af8f705595c1d26e7c4a)
[【Day18】インスタ用のキャプションを生成する（後編）](https://qiita.com/midorigame/items/9a0cbc48b346055c0a12)

## 前回やったこと

2つ目の機能として、画像から抽出したExif情報をインスタグラムのキャプション用に加工して表示させる機能を実装しました！

## 今回やったこと

もう少し機能やUIをブラッシュアップしたい気持ちがありますが、最低限はできたと思うので次は自動テストを用意してコード品質を高めてみることにしました！

普段の業務でもテストコードは意識するように言われているので、何かしら業務にも生かせる学びを得たいなぁと思っています。

### まずはChat GPTに相談

今回のシステム構成（フロント：React、バック：FastAPI）ではどんなテストを実装してみればいいのか？を聞いてみました！

```text:回答
✅ 1. テストの種類と目的

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【テストの種類】     | 【対象】                        | 【目的】                                              | 【使用ツール例】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ユニットテスト       | 単一の関数・コンポーネント      | ・ロジック単体が正しく動くか                         | pytest, Jest
────────────────────────────────────────────────────────────────────────
コンポーネントテスト | Reactコンポーネント単体         | ・UI構造や状態管理が意図通りか                       | React Testing Library, Jest
────────────────────────────────────────────────────────────────────────
APIテスト            | PythonバックエンドのAPI         | ・正しいリクエストとレスポンスが行われるか           | pytest + httpx, supertest
────────────────────────────────────────────────────────────────────────
結合テスト           | フロントエンド ↔ バックエンド   | ・通信含めた機能全体が正しく動くか                   | Playwright, Cypress
────────────────────────────────────────────────────────────────────────
E2E（エンドツーエンド）
テスト               | ユーザーの操作フロー全体        | ・ユーザー目線で期待どおりにアプリが動くか           | Playwright, Cypress
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

```

さらにもうちょっと具体的なテスト内容まで考えてもらいました。

```text:回答
📌 実装すべき自動テスト一覧

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【レイヤー】         | 【対象】                        | 【テスト項目】                                           | 【使用ツール】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
APIユニットテスト    | FastAPIの各エンドポイント      | ・ステータスコード  
                    |                                | ・Content-Type  
                    |                                | ・出力フォーマット  
                    |                                | ・エラーハンドリング                                     | pytest + httpx.AsyncClient
──────────────────────────────────────────────────────────
画像生成テスト       | /generate-framed-image API      | ・画像形式の正当性  
                    |                                | ・サイズの検証  
                    |                                | ・出力画像の画素数チェック（余裕があれば）               | pytest + Pillow
──────────────────────────────────────────────────────────
Exifキャプション     | /generate-caption API           | ・JSON形式の検証  
テスト              |                                | ・ハッシュタグの正しさ（#付き、記号除去）                | pytest
──────────────────────────────────────────────────────────
Reactコンポーネント | UI表示部分（画像・キャプション）| ・初期描画確認  
テスト              |                                | ・ファイル選択後の挙動  
                    |                                | ・APIレスポンスの反映                                   | React Testing Library + Jest
──────────────────────────────────────────────────────────
E2Eテスト           | 全体のフロー（UI→API）         | ・画像アップロードから表示までの一連操作の確認           | Playwright または Cypress
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

```

最初はロジックがちゃんと動くかを検証して、そのあと仕様に沿っているかを確認する流れですね。
この方針で順番にテストを実装していきましょう！！

## APIユニットテストの実装

以下のようなディレクトリ構成でテストを実装しました！

```text:backendのディレクトリ構成
./backend/
├── app
│   ├── api
│   │   └── v1
│   │       └── endpoints
│   │           ├── generate.py　☆ APIユニットテストの対象
│   │           └── upload.py　　☆ APIユニットテストの対象
│   ├── core
│   │   └── config.py
│   ├── models
│   │   └── req_exif.py
│   ├── services
│   │   ├── caption_generatior
│   │   │   ├── __init__.py
│   │   │   └── generate_captiopn.py
│   │   └── image_processor
│   │       ├── __init__.py
│   │       ├── frame_composer.py
│   │       ├── image_drawer.py
│   │       └── text_drawer.py
│   ├── utils
│   │   ├── __init__.py
│   │   └── exif_utils.py
│   └── main.py
├── tests
│   └── api
│       ├── test_generate.py　☆ APIユニットテストのコード
│       └── test_upload.py　　☆ APIユニットテストのコード
│
└── static
    └── font
        └── Montserrat
```

テストコードはAPIに接続できるかどうかのテストを行うので、レスポンスで200が返ってくるか、意図した形式で帰ってくるかだけを確認しています。

```python:test_upload.py
import pytest
from httpx import AsyncClient
from httpx._transports.asgi import ASGITransport
from PIL import Image
import io
import json
from app.main import app  # FastAPI appにrouterが含まれている前提

@pytest.mark.asyncio
async def test_upload_image_success():
    # 1. ダミー画像作成
    img = Image.new("RGB", (800, 600), color="blue")
    buf = io.BytesIO()
    img.save(buf, format="JPEG")
    buf.seek(0)

    # 2. ダミーEXIFデータ
    exif_dict = {
        "Model": "Nikon Zf",
        "LensModel": "Nikkor Z 40mm f/2",
        "FNumber": 2.0,
        "ExposureTime": "1/125",
        "ISOSpeedRatings": 100,
        "DateTime": "2023:06:01 10:00:00"
    }
    exif_json = json.dumps(exif_dict)

    transport = ASGITransport(app=app)

    # 3. HTTPリクエスト送信
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        response = await ac.post(
            "/v1/upload-image",
            files={"file": ("test.jpg", buf, "image/jpeg")},
            data={
                "show_exif": "true",
                "exif": exif_json
            }
        )

    # 4. レスポンス検証
    assert response.status_code == 200
    assert response.headers["content-type"] == "image/png"
    assert len(response.content) > 1000  # サイズチェック
```

testsディレクトリ以下に配置したテストコードは以下のコマンドで実行します。
```bash
cd backend/
env PYTHONPATH=. pytest tests/api/test_upload.py  # テストコード単体で実行
env PYTHONPATH=. pytest tests/  # tests以下にある接頭語がtestのコードをすべて実行
```


## おわりに

今作っているWebアプリにはどんなテストが必要かを調査して、APIユニットテストを実装してみました！
次回以降もテスト実装していきたいと思います！

また、余談ですが、諸事情により毎日更新が18日で止まってしまいました、、、悔しい～～～～～
今日から改めて6月中は毎日投稿がんばります！！