はじめに

GitHub Copilotを活用した開発において、「どこから実装を始めるべきか」「どのような順序で進めればよいか」という問いに悩んだことはないでしょうか。AIにコードを生成させても、全体のアーキテクチャとの整合性が取れなかったり、テストが後回しになったりする課題が生じがちです。

Outside-in TDDと**BDD(振る舞い駆動開発)**は、この問題を解決する強力なアプローチです。ユーザーの視点から要件を定義し、受け入れテストを起点として段階的に内部実装へと進むことで、設計とテストが自然に統合された開発フローを実現できます。

本記事では、GitHub Copilotのカスタムエージェントを開発フェーズごとに使い分けることで、Outside-in TDDとBDDの原則に沿ったAI駆動開発ワークフローを構築する方法を解説します。React(フロントエンド)とNestJS(バックエンド)のフルスタック構成を題材に、要件定義からテスト駆動実装までを自動化する実践的なガイドです。

Outside-in TDDとBDDの開発フロー概要

Outside-in TDDとBDDを組み合わせた開発では、以下のフェーズを順に進行します。

flowchart TD
    subgraph "Phase 1: Discovery & Formulation"
        A[ユーザーストーリー定義] --> B[受け入れ基準の策定]
        B --> C[Gherkin形式でシナリオ記述]
    end
    
    subgraph "Phase 2: Outside-in TDD"
        C --> D[E2Eテスト作成 - Red]
        D --> E[フロントエンドテスト - Red]
        E --> F[バックエンドテスト - Red]
        F --> G[内側から実装を進める]
        G --> H[全テストがGreen]
    end
    
    subgraph "Phase 3: Refactor & Integration"
        H --> I[リファクタリング]
        I --> J[統合検証]
    end
    
    style A fill:#e3f2fd,stroke:#1565c0,color:#000000
    style B fill:#e3f2fd,stroke:#1565c0,color:#000000
    style C fill:#e3f2fd,stroke:#1565c0,color:#000000
    style D fill:#ffcdd2,stroke:#c62828,color:#000000
    style E fill:#ffcdd2,stroke:#c62828,color:#000000
    style F fill:#ffcdd2,stroke:#c62828,color:#000000
    style G fill:#fff3e0,stroke:#e65100,color:#000000
    style H fill:#c8e6c9,stroke:#2e7d32,color:#000000
    style I fill:#e1bee7,stroke:#7b1fa2,color:#000000
    style J fill:#e1bee7,stroke:#7b1fa2,color:#000000

各フェーズでは異なる思考モードとツールセットが求められます。GitHub Copilotのカスタムエージェントを活用することで、フェーズごとに最適化されたAI支援を受けられます。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Node.js 20以上
npm 10以上
VS Code 1.106以降(Custom Agent機能がGA)
GitHub Copilot拡張機能 最新版
React 19.x(Vite環境)
NestJS 11.x
Vitest 3.x
Playwright 1.49以上
OS Windows / macOS / Linux

事前に以下の準備を完了してください。

  • GitHub Copilotのサブスクリプション(Individual、Business、Enterprise、または無料プラン)
  • React + NestJSのモノレポ構成(例: pnpm workspaces、Nx、Turborepo)
  • Playwright、Vitest、Jestの基本的な理解

カスタムエージェント設計の全体像

Outside-in TDD + BDDワークフローをCopilotで自走させるため、以下の5つのカスタムエージェントを作成します。

エージェント名 役割 フェーズ ツール制限
BDD Analyst ユーザーストーリーからGherkinシナリオを生成 Discovery 読み取り専用
E2E Test Architect 受け入れテスト(Playwright)の設計・実装 Outside Loop テストファイルのみ編集
Frontend TDD Agent Reactコンポーネントのテスト駆動開発 Inside Loop フロントエンド領域のみ
Backend TDD Agent NestJSのテスト駆動開発 Inside Loop バックエンド領域のみ
Refactoring Agent 全テストGreen後のリファクタリング Refactor 全ファイル編集可能
flowchart LR
    subgraph Agents
        BDD[BDD Analyst]
        E2E[E2E Test Architect]
        FE[Frontend TDD Agent]
        BE[Backend TDD Agent]
        REF[Refactoring Agent]
    end
    
    BDD -->|Handoff| E2E
    E2E -->|Handoff| FE
    E2E -->|Handoff| BE
    FE -->|Handoff| REF
    BE -->|Handoff| REF
    
    style BDD fill:#e3f2fd,stroke:#1565c0,color:#000000
    style E2E fill:#ffcdd2,stroke:#c62828,color:#000000
    style FE fill:#fff3e0,stroke:#e65100,color:#000000
    style BE fill:#fff3e0,stroke:#e65100,color:#000000
    style REF fill:#c8e6c9,stroke:#2e7d32,color:#000000

Phase 1: BDD Analystエージェントの構築

最初のフェーズでは、ユーザーストーリーを受け取り、構造化されたGherkinシナリオを生成するエージェントを作成します。

エージェント定義ファイル

.github/agents/bdd-analyst.agent.mdを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
---
name: BDD Analyst
description: ユーザーストーリーからGherkin形式の受け入れ基準とシナリオを生成します
tools: ['search', 'codebase', 'fetch']
model: Claude Sonnet 4
handoffs:
  - label: E2Eテスト設計へ進む
    agent: e2e-test-architect
    prompt: 上記のGherkinシナリオに基づいてPlaywrightのE2Eテストを設計してください。
    send: false
---

# BDD Analyst Instructions

あなたはBDD(振る舞い駆動開発)のエキスパートアナリストです。ユーザーストーリーを受け取り、以下の成果物を生成してください。

## 必須タスク

1. **ユーザーストーリーの明確化**
   - Who(誰が)、What(何を)、Why(なぜ)を明確にする
   - 暗黙の前提条件を洗い出す

2. **受け入れ基準の策定**
   - INVEST原則に従った基準を定義
   - 境界条件とエッジケースを含める

3. **Gherkinシナリオの生成**
   - Given-When-Then形式で記述
   - 正常系と異常系の両方をカバー
   - 具体的な値を使用した例示

## 出力フォーマット

\`\`\`gherkin
Feature: [機能名]
  As a [ユーザータイプ]
  I want [達成したいこと]
  So that [得られる価値]

  Background:
    Given [前提条件]

  Scenario: [正常系シナリオ名]
    Given [初期状態]
    When [アクション]
    Then [期待結果]

  Scenario: [異常系シナリオ名]
    Given [初期状態]
    When [不正なアクション]
    Then [エラー処理の期待結果]
\`\`\`

## 制約事項

- コードの編集は一切行わない
- 既存のコードベースを参照して整合性を確認する
- 技術的な実装詳細には踏み込まない

BDD Analystの使用例

ユーザーストーリーを入力として渡すと、BDD AnalystはGherkinシナリオを生成します。

プロンプト例:

1
2
3
4
5
以下のユーザーストーリーからGherkinシナリオを生成してください。

「ユーザーとして、タスクを作成・編集・削除できるようにしたい。
タスクにはタイトル(必須)、説明(任意)、期限(任意)を設定できる。
期限を過ぎたタスクは一覧で強調表示される。」

生成されるシナリオ例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Feature: タスク管理
  As a ユーザー
  I want タスクを作成・編集・削除できる
  So that 自分のやるべきことを管理できる

  Background:
    Given ユーザーがログイン済みである

  Scenario: タスクを新規作成する
    Given タスク一覧ページを表示している
    When 「新規タスク」ボタンをクリックする
    And タイトルに「レポート作成」と入力する
    And 説明に「月次レポートを作成する」と入力する
    And 期限に「2026-01-15」を設定する
    And 「保存」ボタンをクリックする
    Then タスク一覧に「レポート作成」が表示される
    And 成功メッセージ「タスクを作成しました」が表示される

  Scenario: 必須項目を入力せずにタスクを作成しようとする
    Given タスク作成フォームを表示している
    When タイトルを空のまま「保存」ボタンをクリックする
    Then エラーメッセージ「タイトルは必須です」が表示される
    And タスクは保存されない

  Scenario: 期限切れタスクが強調表示される
    Given 以下のタスクが存在する
      | タイトル     | 期限       |
      | 期限切れタスク | 2026-01-01 |
      | 未来のタスク   | 2026-12-31 |
    And 現在の日付が「2026-01-07」である
    When タスク一覧ページを表示する
    Then 「期限切れタスク」は期限切れスタイルで表示される
    And 「未来のタスク」は通常スタイルで表示される

Phase 2: E2E Test Architectエージェントの構築

BDD Analystが生成したGherkinシナリオを元に、PlaywrightでE2Eテストを設計・実装するエージェントを作成します。

エージェント定義ファイル

.github/agents/e2e-test-architect.agent.mdを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
---
name: E2E Test Architect
description: Gherkinシナリオに基づいてPlaywrightのE2Eテストを設計・実装します
tools: ['search', 'codebase', 'editFiles', 'terminalLastCommand']
model: Claude Sonnet 4
handoffs:
  - label: フロントエンド実装へ
    agent: frontend-tdd-agent
    prompt: E2Eテストを通すために必要なReactコンポーネントをTDDで実装してください。
    send: false
  - label: バックエンド実装へ
    agent: backend-tdd-agent
    prompt: E2Eテストを通すために必要なNestJS APIエンドポイントをTDDで実装してください。
    send: false
---

# E2E Test Architect Instructions

あなたはPlaywrightを使用したE2Eテスト設計のエキスパートです。Outside-in TDDの外側のループとして、受け入れテストを作成します。

## 開発フロー

1. **Gherkinシナリオの解析**
   - 各シナリオをPlaywrightテストケースにマッピング
   - Page Object Modelを活用した構造化

2. **テストファイルの作成**
   - `e2e/`ディレクトリに配置
   - 命名規則: `[feature].spec.ts`

3. **テストの実装**
   - 失敗するテストを先に書く(Red状態)
   - アサーションは具体的に
   - 待機処理は適切に設定

## コード規約

- Playwright公式のベストプラクティスに従う
- ロケーターはアクセシビリティ属性を優先
- ハードコードされた待機時間を避ける
- テストデータはフィクスチャで管理

## 出力形式

\`\`\`typescript
import { test, expect } from '@playwright/test';

test.describe('Feature: タスク管理', () => {
  test.beforeEach(async ({ page }) => {
    // 前提条件のセットアップ
  });

  test('Scenario: タスクを新規作成する', async ({ page }) => {
    // Given-When-Thenの実装
  });
});
\`\`\`

## 制約事項

- テストファイル(`*.spec.ts`, `*.test.ts`)のみ編集可能
- Page Objectクラスの作成は許可
- 実装コード(src/)は編集しない

E2E Test Architectの使用例

プロンプト例:

1
2
3
4
以下のGherkinシナリオに基づいてPlaywrightテストを作成してください。
フロントエンドは http://localhost:5173 で起動しています。

[Gherkinシナリオを貼り付け]

生成されるテスト例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// e2e/task-management.spec.ts
import { test, expect } from '@playwright/test';
import { TaskPage } from './pages/TaskPage';

test.describe('Feature: タスク管理', () => {
  let taskPage: TaskPage;

  test.beforeEach(async ({ page }) => {
    taskPage = new TaskPage(page);
    // Background: ユーザーがログイン済みである
    await taskPage.login('test@example.com', 'password');
  });

  test('Scenario: タスクを新規作成する', async ({ page }) => {
    // Given: タスク一覧ページを表示している
    await taskPage.navigateToList();

    // When: 「新規タスク」ボタンをクリックする
    await taskPage.clickNewTaskButton();

    // And: タイトルに「レポート作成」と入力する
    await taskPage.fillTitle('レポート作成');

    // And: 説明に「月次レポートを作成する」と入力する
    await taskPage.fillDescription('月次レポートを作成する');

    // And: 期限に「2026-01-15」を設定する
    await taskPage.fillDueDate('2026-01-15');

    // And: 「保存」ボタンをクリックする
    await taskPage.clickSaveButton();

    // Then: タスク一覧に「レポート作成」が表示される
    await expect(taskPage.getTaskByTitle('レポート作成')).toBeVisible();

    // And: 成功メッセージが表示される
    await expect(taskPage.successMessage).toHaveText('タスクを作成しました');
  });

  test('Scenario: 必須項目を入力せずにタスクを作成しようとする', async ({ page }) => {
    // Given: タスク作成フォームを表示している
    await taskPage.navigateToList();
    await taskPage.clickNewTaskButton();

    // When: タイトルを空のまま「保存」ボタンをクリックする
    await taskPage.clickSaveButton();

    // Then: エラーメッセージが表示される
    await expect(taskPage.errorMessage).toHaveText('タイトルは必須です');
  });

  test('Scenario: 期限切れタスクが強調表示される', async ({ page }) => {
    // Given: 期限切れタスクと未来のタスクが存在する
    await taskPage.createTask({ title: '期限切れタスク', dueDate: '2026-01-01' });
    await taskPage.createTask({ title: '未来のタスク', dueDate: '2026-12-31' });

    // When: タスク一覧ページを表示する
    await taskPage.navigateToList();

    // Then: 期限切れタスクは期限切れスタイルで表示される
    const overdueTask = taskPage.getTaskByTitle('期限切れタスク');
    await expect(overdueTask).toHaveClass(/overdue/);

    // And: 未来のタスクは通常スタイルで表示される
    const futureTask = taskPage.getTaskByTitle('未来のタスク');
    await expect(futureTask).not.toHaveClass(/overdue/);
  });
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// e2e/pages/TaskPage.ts
import { Page, Locator } from '@playwright/test';

export class TaskPage {
  readonly page: Page;
  readonly newTaskButton: Locator;
  readonly titleInput: Locator;
  readonly descriptionInput: Locator;
  readonly dueDateInput: Locator;
  readonly saveButton: Locator;
  readonly successMessage: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.newTaskButton = page.getByRole('button', { name: '新規タスク' });
    this.titleInput = page.getByLabel('タイトル');
    this.descriptionInput = page.getByLabel('説明');
    this.dueDateInput = page.getByLabel('期限');
    this.saveButton = page.getByRole('button', { name: '保存' });
    this.successMessage = page.getByRole('alert').filter({ hasText: /成功/ });
    this.errorMessage = page.getByRole('alert').filter({ hasText: /エラー|必須/ });
  }

  async login(email: string, password: string) {
    await this.page.goto('/login');
    await this.page.getByLabel('メールアドレス').fill(email);
    await this.page.getByLabel('パスワード').fill(password);
    await this.page.getByRole('button', { name: 'ログイン' }).click();
    await this.page.waitForURL('/tasks');
  }

  async navigateToList() {
    await this.page.goto('/tasks');
  }

  async clickNewTaskButton() {
    await this.newTaskButton.click();
  }

  async fillTitle(title: string) {
    await this.titleInput.fill(title);
  }

  async fillDescription(description: string) {
    await this.descriptionInput.fill(description);
  }

  async fillDueDate(date: string) {
    await this.dueDateInput.fill(date);
  }

  async clickSaveButton() {
    await this.saveButton.click();
  }

  getTaskByTitle(title: string): Locator {
    return this.page.getByRole('listitem').filter({ hasText: title });
  }

  async createTask(task: { title: string; description?: string; dueDate?: string }) {
    await this.clickNewTaskButton();
    await this.fillTitle(task.title);
    if (task.description) await this.fillDescription(task.description);
    if (task.dueDate) await this.fillDueDate(task.dueDate);
    await this.clickSaveButton();
  }
}

Phase 3: Frontend TDD Agentエージェントの構築

E2Eテストが失敗する状態(Red)から、Reactコンポーネントをテスト駆動で実装するエージェントを作成します。

エージェント定義ファイル

.github/agents/frontend-tdd-agent.agent.mdを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
---
name: Frontend TDD Agent
description: ReactコンポーネントをVitest + Testing Libraryでテスト駆動開発します
tools: ['search', 'codebase', 'editFiles', 'terminalLastCommand', 'problems']
model: Claude Sonnet 4
handoffs:
  - label: リファクタリングへ
    agent: refactoring-agent
    prompt: 全テストがGreenになりました。コードをリファクタリングしてください。
    send: false
---

# Frontend TDD Agent Instructions

あなたはReact + TypeScriptのTDDエキスパートです。Outside-in TDDの内側のループとして、コンポーネントレベルのテスト駆動開発を行います。

## TDDサイクル(Red-Green-Refactor)

### Red Phase
1. 失敗するテストを1つ書く
2. コンパイルエラーがあれば最小限のスタブを作成
3. テストが期待通り失敗することを確認

### Green Phase
1. テストを通す最小限のコードを書く
2. ハードコードでも構わない
3. 設計の美しさより動作を優先

### Refactor Phase
1. 重複を排除
2. 命名を改善
3. テストが引き続きGreenであることを確認

## テスト記述ルール

- Testing Libraryのクエリ優先順位を守る
  1. getByRole(最優先)
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByTestId(最終手段)

- ユーザー操作は`userEvent`を使用
- 非同期処理は`waitFor`または`findBy*`を使用

## ファイル構成

\`\`\`
apps/frontend/
├── src/
│   ├── components/
│   │   └── TaskForm/
│   │       ├── TaskForm.tsx
│   │       ├── TaskForm.test.tsx
│   │       └── index.ts
│   └── features/
│       └── tasks/
│           ├── TaskList.tsx
│           ├── TaskList.test.tsx
│           └── hooks/
│               └── useTasks.ts
└── vitest.config.ts
\`\`\`

## 制約事項

- `apps/frontend/`ディレクトリ内のみ編集可能
- テストを先に書く(テストなしの実装は禁止)
- E2Eテストは編集しない
- バックエンドへの依存はモックで解決

Frontend TDD Agentの使用例

プロンプト例:

1
2
3
4
5
6
7
E2Eテストを通すために、以下のコンポーネントをTDDで実装してください。

必要なコンポーネント:
- TaskForm: タスク作成フォーム(タイトル、説明、期限の入力)
- TaskList: タスク一覧表示(期限切れの強調表示を含む)

APIクライアントはモックで対応してください。

生成されるテストと実装例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// apps/frontend/src/components/TaskForm/TaskForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { TaskForm } from './TaskForm';

describe('TaskForm', () => {
  it('タイトル、説明、期限の入力フィールドを表示する', () => {
    render(<TaskForm onSubmit={vi.fn()} />);

    expect(screen.getByLabelText('タイトル')).toBeInTheDocument();
    expect(screen.getByLabelText('説明')).toBeInTheDocument();
    expect(screen.getByLabelText('期限')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '保存' })).toBeInTheDocument();
  });

  it('フォーム送信時にonSubmitが入力値とともに呼ばれる', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();
    render(<TaskForm onSubmit={handleSubmit} />);

    await user.type(screen.getByLabelText('タイトル'), 'レポート作成');
    await user.type(screen.getByLabelText('説明'), '月次レポートを作成する');
    await user.type(screen.getByLabelText('期限'), '2026-01-15');
    await user.click(screen.getByRole('button', { name: '保存' }));

    expect(handleSubmit).toHaveBeenCalledWith({
      title: 'レポート作成',
      description: '月次レポートを作成する',
      dueDate: '2026-01-15',
    });
  });

  it('タイトルが空の場合、バリデーションエラーを表示する', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();
    render(<TaskForm onSubmit={handleSubmit} />);

    await user.click(screen.getByRole('button', { name: '保存' }));

    expect(screen.getByRole('alert')).toHaveTextContent('タイトルは必須です');
    expect(handleSubmit).not.toHaveBeenCalled();
  });
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// apps/frontend/src/components/TaskForm/TaskForm.tsx
import { useState, FormEvent } from 'react';

interface TaskFormData {
  title: string;
  description: string;
  dueDate: string;
}

interface TaskFormProps {
  onSubmit: (data: TaskFormData) => void;
}

export function TaskForm({ onSubmit }: TaskFormProps) {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [dueDate, setDueDate] = useState('');
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    setError(null);

    if (!title.trim()) {
      setError('タイトルは必須です');
      return;
    }

    onSubmit({ title, description, dueDate });
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div role="alert">{error}</div>}

      <div>
        <label htmlFor="title">タイトル</label>
        <input
          id="title"
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>

      <div>
        <label htmlFor="description">説明</label>
        <textarea
          id="description"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />
      </div>

      <div>
        <label htmlFor="dueDate">期限</label>
        <input
          id="dueDate"
          type="date"
          value={dueDate}
          onChange={(e) => setDueDate(e.target.value)}
        />
      </div>

      <button type="submit">保存</button>
    </form>
  );
}

Phase 4: Backend TDD Agentエージェントの構築

フロントエンドと並行して、NestJSのAPIエンドポイントをテスト駆動で実装するエージェントを作成します。

エージェント定義ファイル

.github/agents/backend-tdd-agent.agent.mdを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
---
name: Backend TDD Agent
description: NestJS APIエンドポイントをJest + Supertestでテスト駆動開発します
tools: ['search', 'codebase', 'editFiles', 'terminalLastCommand', 'problems']
model: Claude Sonnet 4
handoffs:
  - label: リファクタリングへ
    agent: refactoring-agent
    prompt: 全テストがGreenになりました。コードをリファクタリングしてください。
    send: false
---

# Backend TDD Agent Instructions

あなたはNestJS + TypeScriptのTDDエキスパートです。Outside-in TDDの内側のループとして、APIレベルのテスト駆動開発を行います。

## TDDサイクル

### 外側から内側へ

1. **Controller Test(統合寄り)**
   - HTTPリクエスト/レスポンスの検証
   - Supertestを使用

2. **Service Test(ユニット)**
   - ビジネスロジックの検証
   - リポジトリはモック

3. **Repository/Entity Test(必要に応じて)**
   - データアクセス層の検証

## テスト記述ルール

- Arrange-Act-Assert パターンを使用
- 各テストは独立して実行可能
- テストデータはファクトリ関数で生成
- モックは`@nestjs/testing`の機能を活用

## ファイル構成

\`\`\`
apps/backend/
├── src/
│   ├── tasks/
│   │   ├── tasks.controller.ts
│   │   ├── tasks.controller.spec.ts
│   │   ├── tasks.service.ts
│   │   ├── tasks.service.spec.ts
│   │   ├── tasks.module.ts
│   │   ├── dto/
│   │   │   ├── create-task.dto.ts
│   │   │   └── update-task.dto.ts
│   │   └── entities/
│   │       └── task.entity.ts
│   └── app.module.ts
└── test/
    └── tasks.e2e-spec.ts
\`\`\`

## 制約事項

- `apps/backend/`ディレクトリ内のみ編集可能
- テストを先に書く
- フロントエンドコードは編集しない
- データベースへの実際の接続はテストでは行わない

Backend TDD Agentの使用例

プロンプト例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
タスク管理APIをTDDで実装してください。

必要なエンドポイント:
- POST /tasks - タスク作成
- GET /tasks - タスク一覧取得
- PUT /tasks/:id - タスク更新
- DELETE /tasks/:id - タスク削除

バリデーション:
- titleは必須、1-100文字
- dueDateはISO 8601形式

生成されるテストと実装例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// apps/backend/src/tasks/tasks.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';

describe('TasksController', () => {
  let controller: TasksController;
  let service: TasksService;

  const mockTasksService = {
    create: jest.fn(),
    findAll: jest.fn(),
    update: jest.fn(),
    remove: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [TasksController],
      providers: [
        {
          provide: TasksService,
          useValue: mockTasksService,
        },
      ],
    }).compile();

    controller = module.get<TasksController>(TasksController);
    service = module.get<TasksService>(TasksService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('create', () => {
    it('タスクを作成してレスポンスを返す', async () => {
      const createTaskDto: CreateTaskDto = {
        title: 'レポート作成',
        description: '月次レポートを作成する',
        dueDate: '2026-01-15',
      };
      const expectedTask = { id: '1', ...createTaskDto, createdAt: new Date() };

      mockTasksService.create.mockResolvedValue(expectedTask);

      const result = await controller.create(createTaskDto);

      expect(service.create).toHaveBeenCalledWith(createTaskDto);
      expect(result).toEqual(expectedTask);
    });
  });

  describe('findAll', () => {
    it('全タスクを取得して返す', async () => {
      const expectedTasks = [
        { id: '1', title: 'タスク1', dueDate: '2026-01-15' },
        { id: '2', title: 'タスク2', dueDate: '2026-01-20' },
      ];

      mockTasksService.findAll.mockResolvedValue(expectedTasks);

      const result = await controller.findAll();

      expect(service.findAll).toHaveBeenCalled();
      expect(result).toEqual(expectedTasks);
    });
  });
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// apps/backend/src/tasks/tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TasksService } from './tasks.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Task } from './entities/task.entity';
import { Repository } from 'typeorm';

describe('TasksService', () => {
  let service: TasksService;
  let repository: Repository<Task>;

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    find: jest.fn(),
    findOne: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TasksService,
        {
          provide: getRepositoryToken(Task),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<TasksService>(TasksService);
    repository = module.get<Repository<Task>>(getRepositoryToken(Task));
  });

  describe('create', () => {
    it('タスクを作成して保存する', async () => {
      const createTaskDto = {
        title: 'レポート作成',
        description: '月次レポートを作成する',
        dueDate: '2026-01-15',
      };
      const task = { id: '1', ...createTaskDto };

      mockRepository.create.mockReturnValue(task);
      mockRepository.save.mockResolvedValue(task);

      const result = await service.create(createTaskDto);

      expect(repository.create).toHaveBeenCalledWith(createTaskDto);
      expect(repository.save).toHaveBeenCalledWith(task);
      expect(result).toEqual(task);
    });
  });
});

Phase 5: Refactoring Agentエージェントの構築

全テストがGreenになった後、コードの品質向上を担当するエージェントを作成します。

エージェント定義ファイル

.github/agents/refactoring-agent.agent.mdを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
---
name: Refactoring Agent
description: 全テストがGreenの状態でコードをリファクタリングします
tools: ['search', 'codebase', 'editFiles', 'terminalLastCommand', 'problems', 'usages']
model: Claude Sonnet 4
---

# Refactoring Agent Instructions

あなたはリファクタリングのエキスパートです。TDDサイクルのRefactorフェーズとして、テストがGreenの状態を維持しながらコードを改善します。

## リファクタリング原則

1. **小さなステップで進める**
   - 一度に一つの改善のみ
   - 各ステップ後にテスト実行

2. **テストを壊さない**
   - 外部インターフェースは維持
   - 振る舞いを変えない

3. **コードの臭いを解消**
   - 重複コードの排除(DRY)
   - 長すぎるメソッドの分割
   - 不適切な命名の改善
   - マジックナンバーの定数化

## 検出すべきコードの臭い

| 臭い | 対処法 |
|------|--------|
| 重複コード | 関数/メソッドに抽出 |
| 長いメソッド | 意味のある単位で分割 |
| 巨大なクラス | 責務を分離 |
| 長い引数リスト | オブジェクトにまとめる |
| 条件分岐の複雑さ | ポリモーフィズムまたはStrategy |
| コメントが多い | コードを自己説明的に |

## リファクタリング手順

1. テストが全てGreenであることを確認
2. 改善箇所を特定
3. リファクタリングを実施
4. テストを再実行してGreenを確認
5. 必要に応じて繰り返し

## 制約事項

- テストが失敗する変更は禁止
- 新機能の追加は禁止
- 既存のテストの削除・変更は原則禁止

ワークフロー全体の自動化

Handoff機能による連携

カスタムエージェント間でHandoff機能を使用することで、開発フェーズの移行をスムーズに行えます。各エージェントのヘッダーに定義したHandoff設定により、次のエージェントへコンテキストを引き継げます。

sequenceDiagram
    participant User as 開発者
    participant BDD as BDD Analyst
    participant E2E as E2E Test Architect
    participant FE as Frontend TDD Agent
    participant BE as Backend TDD Agent
    participant REF as Refactoring Agent
    
    User->>BDD: ユーザーストーリーを入力
    BDD->>BDD: Gherkinシナリオ生成
    BDD-->>E2E: Handoff: E2Eテスト設計へ
    
    E2E->>E2E: Playwrightテスト作成(Red)
    E2E-->>FE: Handoff: フロントエンド実装へ
    E2E-->>BE: Handoff: バックエンド実装へ
    
    par フロントエンドTDD
        FE->>FE: コンポーネントテスト(Red)
        FE->>FE: 実装(Green)
    and バックエンドTDD
        BE->>BE: APIテスト(Red)
        BE->>BE: 実装(Green)
    end
    
    FE-->>REF: Handoff: リファクタリングへ
    BE-->>REF: Handoff: リファクタリングへ
    
    REF->>REF: コード改善
    REF->>User: 完成

ワークフロー実行のプロンプト例

開発の開始から完了までの一連のプロンプト例を示します。

1. BDD Analystへの入力:

1
2
3
4
5
6
7
以下の機能要件からGherkinシナリオを生成してください。

機能: ユーザー認証
- メールアドレスとパスワードでログインできる
- ログイン失敗時はエラーメッセージを表示
- ログイン成功後はダッシュボードにリダイレクト
- 30日間ログイン状態を保持するオプション

2. E2E Test Architectへの入力(Handoff後):

1
2
3
4
5
6
上記のGherkinシナリオに基づいてPlaywrightテストを作成してください。

技術スタック:
- フロントエンド: React 19 + Vite (localhost:5173)
- バックエンド: NestJS 11 (localhost:3000)
- 認証: JWT

3. Frontend TDD Agentへの入力(Handoff後):

1
2
3
4
5
6
7
E2Eテストを通すために、以下のコンポーネントをTDDで実装してください。

- LoginForm: メールアドレス、パスワード、Remember Me チェックボックス
- useAuth: 認証状態を管理するカスタムフック
- AuthContext: 認証コンテキストプロバイダー

テストは Vitest + Testing Library を使用してください。

4. Backend TDD Agentへの入力(Handoff後):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
認証APIをTDDで実装してください。

エンドポイント:
- POST /auth/login - ログイン
- POST /auth/logout - ログアウト
- GET /auth/me - 現在のユーザー情報

技術スタック:
- NestJS 11
- Passport.js + JWT
- class-validator

テストは Jest + Supertest を使用してください。

5. Refactoring Agentへの入力(全テストGreen後):

1
2
3
4
5
6
7
8
認証機能の実装が完了しました。以下の観点でリファクタリングを実施してください。

- 重複コードの排除
- エラーハンドリングの統一
- 型定義の改善
- コンポーネントの責務分離

テストが全てGreenを維持することを確認してください。

プロジェクト構成のベストプラクティス

Outside-in TDD + BDDワークフローを効果的に実践するためのプロジェクト構成を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
project-root/
├── .github/
│   ├── agents/
│   │   ├── bdd-analyst.agent.md
│   │   ├── e2e-test-architect.agent.md
│   │   ├── frontend-tdd-agent.agent.md
│   │   ├── backend-tdd-agent.agent.md
│   │   └── refactoring-agent.agent.md
│   ├── copilot-instructions.md
│   └── prompts/
│       ├── generate-component.prompt.md
│       └── generate-api-endpoint.prompt.md
├── apps/
│   ├── frontend/
│   │   ├── src/
│   │   ├── vitest.config.ts
│   │   └── package.json
│   └── backend/
│       ├── src/
│       ├── test/
│       └── package.json
├── e2e/
│   ├── specs/
│   │   └── *.spec.ts
│   ├── pages/
│   │   └── *.ts
│   ├── fixtures/
│   └── playwright.config.ts
├── docs/
│   └── features/
│       └── *.feature(Gherkinファイル)
├── package.json
└── pnpm-workspace.yaml

Outside-in TDDとBDDをAI駆動開発で成功させるポイント

エージェントの責務を明確に分離する

各エージェントには明確な責務範囲を設定し、ツールアクセスを制限することで、意図しないファイル変更を防ぎます。計画フェーズでは読み取り専用、実装フェーズでは対象ディレクトリのみ編集可能にするなど、段階的な権限設定が重要です。

テストを先に書く原則を徹底する

AIは「動くコード」を素早く生成できますが、テストなしの実装はOutside-in TDDの原則に反します。カスタムエージェントの指示に「テストを先に書く」ことを明記し、Red-Green-Refactorサイクルを遵守させましょう。

Gherkinシナリオをシングルソースオブトゥルースとする

BDD AnalystでGherkinシナリオを作成したら、それをすべての実装の起点とします。E2Eテスト、コンポーネントテスト、APIテストは、Gherkinシナリオで定義された振る舞いを検証する形で記述します。

継続的なフィードバックループを維持する

テストが失敗したまま長時間放置せず、小さなステップで進めます。GitHub Copilotのターミナル統合機能を活用し、テスト実行結果を即座にフィードバックとして受け取れる環境を構築しましょう。

まとめ

本記事では、Outside-in TDDとBDDの原則をGitHub Copilotのカスタムエージェントで実践する方法を解説しました。

開発フェーズごとに最適化された5つのカスタムエージェント(BDD Analyst、E2E Test Architect、Frontend TDD Agent、Backend TDD Agent、Refactoring Agent)を構築することで、AIに開発の各段階を自走させながら、品質の高いテスト駆動開発を実現できます。

ポイントをまとめると以下のとおりです。

  • Outside-in TDDは外側(ユーザー視点)から内側(実装詳細)へ進むアプローチ
  • BDDはGherkinシナリオで振る舞いを定義し、実行可能な仕様書として活用
  • カスタムエージェントはフェーズごとにツールと指示を最適化
  • Handoff機能でエージェント間のコンテキスト引き継ぎを自動化
  • テストを先に書く原則をAIにも徹底させる

このワークフローを導入することで、GitHub Copilotを単なるコード補完ツールではなく、設計とテストを統合したAI開発パートナーとして活用できるようになります。

参考リンク