---
title: "テスト設計書"
---

## 概要

MorningStatusApp のテストは [Vitest](https://vitest.dev/) で実行する。
テストコマンド: `bun run test`

---

## テスト分類

| 分類             | 対象                                         | 配置場所                |
| ---------------- | -------------------------------------------- | ----------------------- |
| ユニットテスト   | ライブラリ関数・ユーティリティ               | `lib/*.test.ts`         |
| コンポーネントテスト | React コンポーネント（UI 表示・操作）   | `components/*.test.tsx` |
| ページテスト     | Next.js ページ（Server Component）           | `app/**/*.test.tsx`     |
| API テスト       | Next.js API Route（GET/PUT）                 | `app/api/**/*.test.ts`  |
| スクリプトテスト | バッチスクリプト（同期処理）                 | `scripts/*.test.ts`     |

---

## テストファイル一覧

### ライブラリ（`lib/`）

| ファイル                          | テスト対象                                      | 主なテスト項目                                           |
| --------------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
| `lib/blob.test.ts`                | `getMembersFromBlob()`                          | Blob フェッチ成功・失敗・バリデーションエラー             |
| `lib/releases.test.ts`            | `getReleases()`                                 | Prisma フィールドマッピング・null→undefined 変換・TrackLink→ExternalLink |
| `lib/date.test.ts`                | `formatJST()`                                   | JST 変換・空文字・無効値                                  |
| `lib/sns.test.ts`                 | `resolveSnsCheck()` / `hasOfficialSnsSourceMatch()` | 各 snsCheck 状態の解決・SNS ドメイン一致判定          |
| `lib/member-release-linker.test.ts` | `linkMemberToReleases()`                      | 在籍期間とリリース日の重複判定・overrides 適用            |
| `lib/musicbrainz.test.ts`         | `fetchReleaseGroupsByArtist()`                  | API 正常取得・レート制限（429）再試行・JSON 解析エラー    |
| `lib/release-normalizer.test.ts`  | `normalizeReleases()` / `deduplicateReleases()` | 正規化・重複排除・releaseDate のスペース処理              |

### コンポーネント（`components/`）

| ファイル                          | テスト対象         | 主なテスト項目                                                      |
| --------------------------------- | ------------------ | ------------------------------------------------------------------- |
| `components/MemberCard.test.tsx`  | `MemberCard`       | 氏名・世代・ステータス・SNS バッジ・最終更新日の表示                |
| `components/MemberDetailView.test.tsx` | `MemberDetailView` | プロフィール表示・編集フォーム・保存・キャンセル・statusHistory 折りたたみ |
| `components/MemberSelect.test.tsx` | `MemberSelect`    | 検索フィルタ（名前・読みの前方一致）・フィルタ解除                  |

### ページ（`app/`）

| ファイル                             | テスト対象           | 主なテスト項目                                                    |
| ------------------------------------ | -------------------- | ----------------------------------------------------------------- |
| `app/page.test.tsx`                  | `Home`（トップ）     | メンバー件数・最終更新日・バッチサマリ・isRecentlyUpdated 境界値  |
| `app/members/[id]/page.test.tsx`     | `MemberDetail`       | メンバー表示・notFound・前後ナビゲーション・関連リリース表示       |
| `app/releases/page.test.tsx`         | `ReleasesPage`       | フォーマット別グルーピング・データなし表示                        |
| `app/releases/[id]/page.test.tsx`    | `ReleaseDetailPage`  | リリース詳細表示・notFound・関連メンバー表示                      |

### API Route（`app/api/`）

| ファイル                            | テスト対象          | 主なテスト項目                                               |
| ----------------------------------- | ------------------- | ------------------------------------------------------------ |
| `app/api/members/route.test.ts`     | GET / PUT `/api/members` | Blob フェッチ・保存成功・MEMBERS_BLOB_URL 未設定エラー・statusHistory 蓄積・上限100件 |

### スクリプト（`scripts/`）

| ファイル                              | テスト対象              | 主なテスト項目                                                              |
| ------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
| `scripts/sync-status.test.ts`         | `syncMembers()` / `fetchMemberStatus()` / `performSearch()` | SNS 検索・Web 検索・日本語判定・snsCheck 更新・active フラグ不変・スキップ・曜日制御・ドライラン |
| `scripts/sync-discography.test.ts`    | `syncDiscography()`     | MusicBrainz 取得・正規化・Blob 保存・ドライラン                             |
| `scripts/migrate-to-blob.test.ts`     | `migrateToBlob()`       | ローカルファイル読み込み・Blob 書き込み                                     |
| `scripts/patch-official-sns.test.ts`  | `patchOfficialSns()`    | officialSns マージ・型変換・mergedCount 0件                                |

---

## テスト実装指針

### モックパターン

| 対象                   | モック方法                                                    |
| ---------------------- | ------------------------------------------------------------- |
| `fetch`（グローバル）  | `vi.stubGlobal('fetch', vi.fn().mockResolvedValue({...}))` |
| 環境変数               | `vi.stubEnv('VAR_NAME', 'value')`                            |
| Vercel Blob            | `vi.mock('@vercel/blob', () => ({ put: vi.fn() }))`          |
| exa-js（クラス）       | `vi.mock('exa-js', ...)` + `mockImplementation(function() {...})` |
| `Date`のみ偽装         | `vi.useFakeTimers({ toFake: ['Date'] })` + `vi.setSystemTime(...)` |

> `fetch` モックには必ず `ok: true` を含めること（省略すると正常系が全失敗する）。

> exa-js のようなクラスコンストラクタのモックは `function` キーワードを使うこと
>（アロー関数では「is not a constructor」エラーになる）。

### クリーンアップ

```typescript
afterEach(() => {
  vi.unstubAllGlobals();
  vi.unstubAllEnvs();
  vi.useRealTimers();   // useFakeTimers を使った場合
  // vi.restoreAllMocks() はタイマーを戻さないため useRealTimers が別途必要
});
```

### Server Component のテスト

Server Component（`async function Page()`）は `render()` を使わず直接 `await` して結果を検証する。

```typescript
const result = await Home();
const html = JSON.stringify(result);
expect(html).toContain('"isRecentlyUpdated":true');
```

> `next/link` の `Link` は `JSON.stringify` で循環参照エラーになるため `vi.mock('next/link', ...)` で差し替えること。

### 曜日依存ロジックのテスト

曜日によって結果が変わるロジック（`shouldSyncOG()` 等）は Date を固定してテストする。

```typescript
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date('2026-01-05T11:30:00Z')); // 月曜日
```

### 再試行ロジックのテスト

`setTimeout` を含む再試行ループは `vi.useFakeTimers()` 全面適用 + `vi.runAllTimersAsync()` で完走させる。

---

## テスト実行

```bash
bun run test            # 全テスト実行
bun run test --watch    # ウォッチモード
bun run test path/to/file.test.ts  # 特定ファイルのみ
```

> CI では `bun run lint`（0 warnings）・`bun run type-check` も合わせて通過させること。
