開発
テスト戦略と導入方針
テスト戦略と導入方針
1. 現状
BluePeriodは現在ゼロテスト環境です。自動テストが存在せず、開発者の手動テストに依存しています。
リスク:
- リグレッション(既存機能の意図しない破壊)を防げない
- リファクタリングが困難
- CI/CDでの品質チェックが不可能
2. テスト導入の基本方針
2.1. 段階的導入
一度に全てをテストするのではなく、以下の順序で導入します:
- フェーズ1: Service層のユニットテスト(最優先)
- フェーズ2: ツールの統合テスト
- フェーズ3: コンポーネントのテスト(将来)
2.2. テストフレームワーク
Bun Testを採用します。
理由:
- Bunに既に移行済み
- Jest互換のAPIで学習コストが低い
- 高速(実行時間が短い)
- 組み込みのモック機能
- TypeScriptサポートが優れている
3. テストの種類と役割
┌─────────────────────────────────────────────────────────────┐
│ テストピラミッド │
├─────────────────────────────────────────────────────────────┤
│ E2Eテスト (少) │
│ ┌─────────┐ │
│ │ 統合テスト │ Agent + Tool + Service の結合テスト │
│ └─────────┘ │
│ ┌───────┐ │
│ │ユニットテスト│ Service, Tool 個別のテスト(多数) │
│ └───────┘ │
└─────────────────────────────────────────────────────────────┘3.1. ユニットテスト
対象: Service層、純粋関数 目的: 個々の関数が正しく動作することを保証
// 例: LocalProjectService.getProjectMetadata()
describe("LocalProjectService.getProjectMetadata", () => {
it("should return project metadata", async () => {
const result = await LocalProjectService.getProjectMetadata("test-id");
expect(result).toEqual({
id: "test-id",
title: "Test Project",
author: "Test Author",
// ...
});
});
});3.2. 統合テスト
対象: ツール、エージェント 目的: Service、Tool、Agentが正しく連携することを保証
// 例: blueperiod_get_project tool
describe("blueperiod_get_project tool", () => {
it("should retrieve lightweight project data", async () => {
const tool = createProjectTools(mockGet, mockSet)[1]; // get_project
const result = await tool.func({ projectId: "test-id" });
expect(result).toContain("Test Project");
});
});3.3. E2Eテスト
対象: ユーザー操作的なシナリオ 目的: システム全体が正しく動作することを保証 注: 導入の後半で検討
4. ディレクトリ構造
next-app/src/
├── lib/
│ └── features/
│ ├── projects/
│ │ ├── local-project-service.ts
│ │ └── __tests__/ # Serviceのテスト
│ │ ├── local-project-service.test.ts
│ │ └── tools.test.ts
│ ├── editorial/
│ │ ├── tools/
│ │ │ ├── plot-tools.ts
│ │ │ └── __tests__/ # Toolのテスト
│ │ │ ├── plot-tools.test.ts
│ │ │ └── manuscript-tools.test.ts
│ └── characters/
│ ├── tools.ts
│ └── __tests__/
│ └── tools.test.ts
├── ai/
│ └── agents/
│ └── __tests__/ # Agentのテスト
│ └── assistantGraph.test.ts5. モック戦略
5.1. IndexedDBのモック
db (Dexie.js) をモックする必要があります:
// __mocks__/db.ts
export const db = {
projects: {
get: mock(() => Promise.resolve({})),
toArray: mock(() => Promise.resolve([])),
// ...
}
};5.2. Jotai Atomのモック
const mockGet = mock((atom) => mockValue);
const mockSet = mock();6. 最初のテスト作成タスク
タスク1: テスト環境のセットアップ
package.jsonにテストスクリプトを追加- モック用ファイルを作成
- CI設定にテスト実行を追加
タスク2: Service層のテスト(優先度高)
以下のServiceから順にテストを追加:
-
LocalProjectServicegetProjectMetadata()getLightweightProject()listProjects()
-
ManuscriptServicegetContent()updateContent()
-
PlotServicegetStructuredPlot()addPlotItem()
タスク3: ツールのテスト
createProjectTools()の各ツールcreatePlotTools()の各ツールcreateManuscriptTools()の各ツール
7. 開発プロセスへの組み込み
7.1. 新規機能開発時
1. まずテストを書く(TDD推奨だが、必須ではない)
2. 実装する
3. テストをパスすることを確認
4. PR時にテスト結果を表示7.2. バグ修正時
1. バグを再現するテストを書く
2. バグを修正する
3. テストがパスすることを確認
4. 同様のバグが発生しないことを保証7.3. CI/CDでの実行
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun test8. カバレッジ目標
| フェーズ | カバレッジ目標 | 期限 |
|---|---|---|
| フェーズ1完了時 | Service層: 60% | 1ヶ月 |
| フェーズ2完了時 | Service + Tools: 50% | 2ヶ月 |
| フェーズ3完了時 | 全体: 40% | 将来検討 |
注: 100%を目指すのではなく、重要なパスを確実にカバーすることを優先。
9. 最初のテスト実装例
9.1. LocalProjectService.getProjectMetadata()
// next-app/src/lib/features/projects/__tests__/local-project-service.test.ts
import { describe, it, expect, mock } from "bun:test";
import { db } from "@/lib/db";
// モックのセットアップ
mock.module "@/lib/db", () => ({
db: {
projects: {
get: (id: string) => Promise.resolve({
id,
title: "Test Project",
author: "Test Author",
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01")
})
}
}
});
describe("LocalProjectService", () => {
describe("getProjectMetadata", () => {
it("should return project metadata without content", async () => {
const { LocalProjectService } = await import("../local-project-service");
const result = await LocalProjectService.getProjectMetadata("test-id");
expect(result).toEqual({
id: "test-id",
title: "Test Project",
author: "Test Author",
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01")
});
});
it("should return null for non-existent project", async () => {
// モックをnullを返すように上書き
const result = await LocalProjectService.getProjectMetadata("non-existent");
expect(result).toBeNull();
});
});
});9.2. Toolのテスト例
// next-app/src/lib/features/projects/__tests__/tools.test.ts
import { describe, it, expect } from "bun:test";
describe("createProjectTools", () => {
it("should create tools with blueperiod_ prefix", () => {
const mockGet = () => ({});
const mockSet = () => {};
const { createProjectTools } = await import("../tools");
const tools = createProjectTools(mockGet, mockSet);
expect(tools[0].name).toBe("blueperiod_list_projects");
expect(tools[1].name).toBe("blueperiod_get_project");
// ...
});
});10. 開発体制の定義
10.1. 役割と責任
| 役割 | 責任 |
|---|---|
| 開発者 | 実装と同時にテストを作成 |
| レビューアー | テストが十分かどうかをレビュー |
| CI | 自動的にテストを実行 |
10.2. テスト未追加の場合の対応
- レビュアーはテストがない場合、PRを承認しない
- ただし、調査・実験的コードの場合は例外とする
11. まとめ
- Bun Testを採用
- Service層のユニットテストから開始
- モックを活用して外部依存を切り離す
- 段階的に導入し、カバレッジを徐々に上げる
- CI/CDに組み込んで自動実行
次のアクション:
- package.jsonにテストスクリプト追加
- LocalProjectServiceの最初のテストを作成
- CI設定を追加