T-CREATOR

GitHub Copilot 前提のコーディング設計:コメント駆動 → テスト → 実装の最短ループ

GitHub Copilot 前提のコーディング設計:コメント駆動 → テスト → 実装の最短ループ

GitHub Copilot の登場により、開発の進め方が根本的に変わりました。従来の「コードを書く → テストする」というフローから、「意図をコメントで示す → テストコードを生成 → 実装コードを生成」という新しいサイクルが生まれています。

本記事では、GitHub Copilot を活用した最短開発ループの実践方法を解説します。コメント駆動開発(CDD: Comment-Driven Development)の考え方から、具体的なワークフローまで、実例を交えながら紹介していきますね。

背景

GitHub Copilot が変えた開発プロセス

GitHub Copilot は、OpenAI の Codex モデルを活用した AI コーディングアシスタントです。自然言語のコメントから高品質なコードを生成できるため、開発者の作業スタイルに大きな変化をもたらしました。

従来の開発では、エディタで一文字ずつコードを書いていく時間が大半を占めていましたが、Copilot はその時間を大幅に短縮します。開発者は「何を作るか」という設計や意図の明確化に集中でき、「どう書くか」の実装詳細は AI に任せられるようになったのです。

コメント駆動開発の台頭

コメント駆動開発とは、実装コードよりも先にコメントで処理の意図や仕様を記述し、それを起点にコードを生成していく手法です。GitHub Copilot との組み合わせで、この手法が非常に効果的になりました。

コメントは人間が読むためだけでなく、AI への指示としても機能します。明確なコメントを書くことで、Copilot はより正確なコードを提案してくれるでしょう。

以下の図は、従来の開発フローと Copilot を活用した新しいフローの違いを示しています。

mermaidflowchart TB
    subgraph traditional["従来の開発フロー"]
        t1["要件定義"] --> t2["設計"]
        t2 --> t3["実装コーディング"]
        t3 --> t4["テストコード作成"]
        t4 --> t5["デバッグ・修正"]
    end

    subgraph copilot["Copilot 活用フロー"]
        c1["要件定義"] --> c2["コメントで意図記述"]
        c2 --> c3["テストコード生成"]
        c3 --> c4["実装コード生成"]
        c4 --> c5["実行・検証"]
    end

    style copilot fill:#e1f5e1
    style traditional fill:#f5f5f5

図から分かるように、Copilot 活用フローでは「コメント記述」が起点となり、テストと実装が AI によって加速される構造になっています。

TDD との親和性

テスト駆動開発(TDD)は「テストを先に書く → 実装する → リファクタリング」のサイクルを回す手法として知られています。Copilot はこの TDD サイクルとも高い親和性があります。

コメントでテスト仕様を記述すると、Copilot がテストコードを提案してくれます。そのテストを実行して失敗することを確認したら、今度は実装コードのコメントを書いて Copilot に実装を生成させる、という流れがスムーズに実現できるのです。

課題

従来の開発における時間配分の問題

従来の開発では、実装コードの記述に多くの時間が割かれていました。特に定型的な処理や、言語仕様に則った正確な構文の記述には、経験豊富な開発者でも一定の時間が必要です。

また、テストコードの作成も手間がかかります。テストケースを網羅的に記述し、期待値を正確に設定するには、実装と同等以上の時間が必要になることも少なくありません。その結果、テストの作成が後回しになり、品質が犠牲になるケースが多く見られました。

AI 生成コードの品質への懸念

GitHub Copilot が便利である一方、生成されたコードをそのまま使って良いのかという懸念も存在します。AI が提案するコードが要件を満たしているか、セキュリティ上の問題がないか、パフォーマンス面で最適かなど、確認すべき点は多岐にわたります。

特に初心者の場合、生成されたコードの妥当性を判断できず、バグやセキュリティホールを含んだまま本番環境に投入してしまうリスクがあるでしょう。

コメントの質がコード品質を左右する

Copilot は記述されたコメントを元にコードを生成するため、コメントの質が直接的にコード品質に影響します。曖昧なコメントや不正確な指示からは、期待通りのコードが得られません。

しかし、良いコメントを書くこと自体にもスキルが必要です。何をどこまで詳しく書くべきか、どのような表現が Copilot にとって理解しやすいかなど、ノウハウの蓄積が求められます。

以下の図は、コメント品質とコード品質の関係を示しています。

mermaidflowchart LR
    input["開発者の意図"] --> comment["コメント記述"]
    comment --> quality{"コメントの<br/>質"}
    quality -->|明確・詳細| good["高品質な<br/>コード生成"]
    quality -->|曖昧・不足| poor["低品質な<br/>コード生成"]

    good --> test_pass["テスト通過"]
    poor --> test_fail["テスト失敗"]

    test_fail --> refine["コメント<br/>改善"]
    refine --> comment

    style good fill:#d4edda
    style poor fill:#f8d7da

この図が示すように、コメントの質が低いとテストが失敗し、改善のループに入ります。最初から質の高いコメントを書くことが、開発効率を大きく左右するのです。

解決策

コメント駆動 → テスト → 実装の 3 ステップループ

GitHub Copilot を最大限に活用するには、以下の 3 ステップを最短サイクルで回すことが効果的です。

ステップ 1:コメントで意図を明確化

まず実装したい機能や処理の意図を、自然言語のコメントとして記述します。このコメントは以下の要素を含むと効果的でしょう。

#要素説明記述例
1目的何のための処理かユーザー認証を行う
2入力どんなデータを受け取るかメールアドレスとパスワード
3処理内容何をするかデータベースと照合して検証
4出力何を返すか認証トークンまたはエラー
5エッジケース例外的な状況無効な入力時の処理

このようにコメントを構造化することで、Copilot はより正確なコードを提案してくれます。

ステップ 2:テストコードを先に生成

コメントを書いた後、まずテストコードを生成します。TDD の原則に従い、実装よりも先にテストを用意するのです。

テストコードのコメントでは、以下のような情報を記述すると良いでしょう。

typescript// テスト:ユーザー認証関数
// 正常系:有効な認証情報で認証トークンが返される
// 異常系:無効なメールアドレスでエラーが返される
// 異常系:パスワード不一致でエラーが返される

このコメントを書くと、Copilot は Jest や Mocha などのテストフレームワークを使ったテストコードを提案してくれます。

ステップ 3:実装コードを生成して検証

テストが用意できたら、実装コードを生成します。先ほど書いた実装意図のコメントを元に、Copilot に関数やメソッドの本体を提案させるのです。

生成されたコードに対してテストを実行し、すべてのテストケースが通ることを確認します。テストが失敗した場合は、コメントを修正・詳細化して再生成を試みましょう。

以下の図は、この 3 ステップループの流れを表しています。

mermaidstateDiagram-v2
    [*] --> WriteComment:開始
    WriteComment:コメント記述
    GenerateTest:テストコード生成
    GenerateImpl:実装コード生成
    RunTest:テスト実行

    WriteComment --> GenerateTest:Copilot で生成
    GenerateTest --> GenerateImpl:Copilot で生成
    GenerateImpl --> RunTest:実行

    RunTest --> Success:成功
    RunTest --> RefineComment:失敗

    RefineComment:コメント改善
    RefineComment --> GenerateTest:再生成

    Success --> [*]:完了

    note right of WriteComment
        意図を明確に
        構造化して記述
    end note

    note right of RunTest
        全ケース通過で完了
        失敗時は改善ループ
    end note

このループを高速で回すことで、実装時間を大幅に短縮しながら、テストによる品質保証も同時に実現できます。

効果的なコメント記述のパターン

Copilot に正確なコードを生成させるには、コメントの書き方にもコツがあります。以下のパターンを意識すると効果的です。

パターン 1:型情報を含める

TypeScript や型アノテーションをサポートする言語では、コメントに型情報を含めると精度が上がります。

typescript/**
 * ユーザー情報を取得する
 * @param userId - ユーザーID (number)
 * @returns Promise<User> - ユーザーオブジェクト
 * @throws UserNotFoundError - ユーザーが存在しない場合
 */

パターン 2:具体例を示す

抽象的な説明だけでなく、具体例を示すことで Copilot の理解が深まります。

typescript// メールアドレスのバリデーション
// 例:valid@example.com -> true
// 例:invalid-email -> false
// 例:@example.com -> false

パターン 3:アルゴリズムの概要を記述

複雑な処理の場合、アルゴリズムのステップを箇条書きで示すと効果的です。

typescript// 配列内の重複を削除する
// 1. Set を使って重複を除去
// 2. 配列に変換して返す
// 3. 元の配列の順序を保持する

テストファーストの実践手順

GitHub Copilot を活用したテストファースト開発の具体的な手順を紹介します。

手順 1:テストファイルを先に作成

実装ファイルを作る前に、テストファイルを作成します。ファイル名は 関数名.test.tsモジュール名.spec.ts のような命名規則に従いましょう。

手順 2:テストケースをコメントで列挙

各テストケースを日本語コメントで列挙します。網羅性を意識して、正常系・異常系・境界値などを含めると良いでしょう。

typescript// describe: ユーザー登録機能
// test: 有効な入力で新規ユーザーが作成される
// test: 既に存在するメールアドレスでエラーが返される
// test: パスワードが8文字未満でバリデーションエラーになる
// test: 必須フィールドが欠けている場合エラーが返される

手順 3 でテストコードを生成

上記のコメントを書いた状態で Enter キーを押すと、Copilot がテストコードの候補を提案してくれます。提案を受け入れて、テストコードを完成させましょう。

手順 4:テストを実行して失敗を確認

まだ実装がないため、テストは失敗します。これが正しい TDD のスタート地点です。yarn test コマンドでテストを実行して、期待通り失敗することを確認します。

手順 5:実装コードを生成

実装ファイルに移り、コメントを記述して Copilot に実装コードを生成させます。テストが通るように、コメントで指定した仕様を正確に記述することが重要です。

手順 6:テストを実行して成功を確認

実装が完了したら、再度テストを実行します。すべてのテストケースが通ることを確認できれば、この機能の開発は完了です。

具体例

実践例:配列の重複削除関数

実際の開発フローを、配列から重複を削除する関数を例に見ていきましょう。

ステップ 1:テストファイルの作成とコメント記述

まず removeDuplicates.test.ts というテストファイルを作成します。

typescript// テストファイル:removeDuplicates.test.ts

テストケースをコメントで記述します。

typescript// describe: removeDuplicates 関数
// test: 重複のない配列はそのまま返される
// test: 重複要素が削除され一意な要素のみになる
// test: 順序が保持される(最初の出現が残る)
// test: 空配列を渡すと空配列が返される
// test: 数値配列でも文字列配列でも動作する

ステップ 2 によるテストコード生成

上記コメントの下で Enter を押すと、Copilot が以下のようなテストコードを提案してくれます。

typescriptimport { removeDuplicates } from './removeDuplicates';

describe('removeDuplicates 関数', () => {
  test('重複のない配列はそのまま返される', () => {
    const input = [1, 2, 3, 4, 5];
    const result = removeDuplicates(input);
    expect(result).toEqual([1, 2, 3, 4, 5]);
  });

次のテストケースも順次生成されます。

typescripttest('重複要素が削除され一意な要素のみになる', () => {
  const input = [1, 2, 2, 3, 3, 3, 4];
  const result = removeDuplicates(input);
  expect(result).toEqual([1, 2, 3, 4]);
});
typescripttest('順序が保持される(最初の出現が残る)', () => {
  const input = [3, 1, 2, 1, 3];
  const result = removeDuplicates(input);
  expect(result).toEqual([3, 1, 2]);
});
typescripttest('空配列を渡すと空配列が返される', () => {
  const input: number[] = [];
  const result = removeDuplicates(input);
  expect(result).toEqual([]);
});
typescript  test('文字列配列でも動作する', () => {
    const input = ['a', 'b', 'a', 'c', 'b'];
    const result = removeDuplicates(input);
    expect(result).toEqual(['a', 'b', 'c']);
  });
});

ステップ 3:テスト実行で失敗を確認

テストを実行してみましょう。

bashyarn test removeDuplicates.test.ts

まだ実装がないため、以下のようなエラーが出力されます。

bashError: Cannot find module './removeDuplicates'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:885:15)

これは期待通りの失敗です。次に実装を作成していきます。

ステップ 4:実装ファイルの作成とコメント記述

removeDuplicates.ts という実装ファイルを作成し、コメントで仕様を記述します。

typescript/**
 * 配列から重複要素を削除する
 * @param array - 重複を削除したい配列
 * @returns 重複が削除された新しい配列
 *
 * アルゴリズム:
 * 1. Set オブジェクトを使用して重複を自動除去
 * 2. スプレッド構文で配列に戻す
 * 3. 元の配列の順序を維持(最初の出現を保持)
 */

ステップ 5 による実装コード生成

上記コメントの下で関数定義を書き始めると、Copilot が実装を提案してくれます。

typescriptexport function removeDuplicates<T>(array: T[]): T[] {
  return [...new Set(array)];
}

非常にシンプルで効率的な実装が生成されました。TypeScript のジェネリクスも適切に使用されています。

ステップ 6:テスト実行で成功を確認

実装が完了したので、再度テストを実行します。

bashyarn test removeDuplicates.test.ts

すべてのテストが通過することを確認できます。

bashPASS  ./removeDuplicates.test.ts
  removeDuplicates 関数
    ✓ 重複のない配列はそのまま返される (2 ms)
    ✓ 重複要素が削除され一意な要素のみになる (1 ms)
    ✓ 順序が保持される(最初の出現が残る) (1 ms)
    ✓ 空配列を渡すと空配列が返される (1 ms)
    ✓ 文字列配列でも動作する (1 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total

このように、コメント駆動 → テスト → 実装のループを回すことで、わずか数分で機能が完成しました。

実践例 クライアントの実装

次に、もう少し複雑な例として、REST API クライアントの実装を見ていきます。

ステップ 1 クライアントのテスト設計

userApiClient.test.ts を作成し、テストケースをコメントで設計します。

typescript// describe: UserApiClient クラス
// describe: getUser メソッド
//   test: 正常なレスポンスでユーザーオブジェクトが返される
//   test: ユーザーが存在しない場合 404 エラーがスローされる
//   test: ネットワークエラー時にエラーがスローされる
// describe: createUser メソッド
//   test: 正常なリクエストで新規ユーザーが作成される
//   test: バリデーションエラー時に 400 エラーがスローされる
//   test: 重複メールアドレスで 409 エラーがスローされる

ステップ 2:モックを含むテストコード生成

このコメントから、Copilot は以下のようなテストコードを生成してくれます。

typescriptimport { UserApiClient } from './userApiClient';
import axios from 'axios';

// axios をモック化
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

ネストした describe ブロックも適切に生成されます。

typescriptdescribe('UserApiClient クラス', () => {
  let client: UserApiClient;

  beforeEach(() => {
    client = new UserApiClient('https://api.example.com');
    jest.clearAllMocks();
  });

getUser メソッドのテストケース:

typescript  describe('getUser メソッド', () => {
    test('正常なレスポンスでユーザーオブジェクトが返される', async () => {
      const mockUser = { id: 1, name: 'John', email: 'john@example.com' };
      mockedAxios.get.mockResolvedValue({ data: mockUser });

      const result = await client.getUser(1);

      expect(result).toEqual(mockUser);
      expect(mockedAxios.get).toHaveBeenCalledWith('/users/1');
    });
typescripttest('ユーザーが存在しない場合 404 エラーがスローされる', async () => {
  mockedAxios.get.mockRejectedValue({
    response: {
      status: 404,
      data: { message: 'User not found' },
    },
  });

  await expect(client.getUser(999)).rejects.toThrow(
    'User not found'
  );
});
typescript    test('ネットワークエラー時にエラーがスローされる', async () => {
      mockedAxios.get.mockRejectedValue(new Error('Network Error'));

      await expect(client.getUser(1)).rejects.toThrow('Network Error');
    });
  });

createUser メソッドのテストケース:

typescript  describe('createUser メソッド', () => {
    test('正常なリクエストで新規ユーザーが作成される', async () => {
      const newUser = { name: 'Jane', email: 'jane@example.com', password: 'secret123' };
      const createdUser = { id: 2, name: 'Jane', email: 'jane@example.com' };

      mockedAxios.post.mockResolvedValue({ data: createdUser });

      const result = await client.createUser(newUser);

      expect(result).toEqual(createdUser);
      expect(mockedAxios.post).toHaveBeenCalledWith('/users', newUser);
    });
typescripttest('バリデーションエラー時に 400 エラーがスローされる', async () => {
  const invalidUser = {
    name: '',
    email: 'invalid',
    password: '123',
  };

  mockedAxios.post.mockRejectedValue({
    response: {
      status: 400,
      data: { message: 'Validation failed' },
    },
  });

  await expect(
    client.createUser(invalidUser)
  ).rejects.toThrow('Validation failed');
});
typescript    test('重複メールアドレスで 409 エラーがスローされる', async () => {
      const duplicateUser = { name: 'John', email: 'john@example.com', password: 'pass123' };

      mockedAxios.post.mockRejectedValue({
        response: { status: 409, data: { message: 'Email already exists' } }
      });

      await expect(client.createUser(duplicateUser)).rejects.toThrow('Email already exists');
    });
  });
});

ステップ 3:実装コードの生成

テストファイルを保存したら、userApiClient.ts を作成します。

typescript/**
 * ユーザー API クライアント
 * REST API を通じてユーザー情報の取得・作成を行う
 */
import axios, { AxiosInstance } from 'axios';

型定義をコメントで指示します。

typescript/**
 * ユーザーオブジェクトの型定義
 */
interface User {
  id: number;
  name: string;
  email: string;
}
typescript/**
 * 新規ユーザー作成用の型定義
 */
interface CreateUserDto {
  name: string;
  email: string;
  password: string;
}

クラス本体のコメント:

typescript/**
 * UserApiClient クラス
 * axios を使用して API とやり取りする
 */
export class UserApiClient {
  private axiosInstance: AxiosInstance;

  /**
   * コンストラクタ
   * @param baseUrl - API のベース URL
   */
  constructor(baseUrl: string) {
    this.axiosInstance = axios.create({ baseURL: baseUrl });
  }

getUser メソッドのコメントと実装:

typescript  /**
   * ユーザー情報を取得する
   * @param userId - 取得するユーザーの ID
   * @returns Promise<User> - ユーザーオブジェクト
   * @throws エラー時は例外をスロー
   */
  async getUser(userId: number): Promise<User> {
    try {
      const response = await this.axiosInstance.get(`/users/${userId}`);
      return response.data;
    } catch (error: any) {
      if (error.response) {
        throw new Error(error.response.data.message);
      }
      throw error;
    }
  }

createUser メソッドのコメントと実装:

typescript  /**
   * 新規ユーザーを作成する
   * @param userData - 作成するユーザーの情報
   * @returns Promise<User> - 作成されたユーザーオブジェクト
   * @throws エラー時は例外をスロー
   */
  async createUser(userData: CreateUserDto): Promise<User> {
    try {
      const response = await this.axiosInstance.post('/users', userData);
      return response.data;
    } catch (error: any) {
      if (error.response) {
        throw new Error(error.response.data.message);
      }
      throw error;
    }
  }
}

ステップ 4:テスト実行と確認

実装が完了したので、テストを実行します。

bashyarn test userApiClient.test.ts

すべてのテストが通過します。

bashPASS  ./userApiClient.test.ts
  UserApiClient クラス
    getUser メソッド
      ✓ 正常なレスポンスでユーザーオブジェクトが返される (5 ms)
      ✓ ユーザーが存在しない場合 404 エラーがスローされる (2 ms)
      ✓ ネットワークエラー時にエラーがスローされる (1 ms)
    createUser メソッド
      ✓ 正常なリクエストで新規ユーザーが作成される (2 ms)
      ✓ バリデーションエラー時に 400 エラーがスローされる (1 ms)
      ✓ 重複メールアドレスで 409 エラーがスローされる (1 ms)

Test Suites: 1 passed, 1 total
Tests:       6 passed, 6 total

このように複雑な API クライアントも、コメント駆動アプローチで効率的に開発できました。

ワークフロー全体の可視化

開発全体の流れを図で整理すると、以下のようになります。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant Comment as コメント
    participant Copilot as GitHub Copilot
    participant Test as テストコード
    participant Impl as 実装コード
    participant Runner as テストランナー

    Dev->>Comment:テスト仕様を<br/>コメントで記述
    Comment->>Copilot:コメントを解析
    Copilot->>Test:テストコード生成
    Test->>Runner:実行(失敗確認)
    Runner-->>Dev:❌ FAIL(期待通り)

    Dev->>Comment:実装仕様を<br/>コメントで記述
    Comment->>Copilot:コメントを解析
    Copilot->>Impl:実装コード生成
    Impl->>Runner:テスト実行

    alt テスト成功
        Runner-->>Dev:✅ PASS(完了)
    else テスト失敗
        Runner-->>Dev:❌ FAIL
        Dev->>Comment:コメント改善
        Comment->>Copilot:再生成
    end

この図からわかるように、開発者は主にコメントを通じて AI と対話し、テストランナーからのフィードバックを受けながら品質を担保していきます。

まとめ

GitHub Copilot を前提としたコーディング設計では、「コメント駆動 → テスト → 実装」の 3 ステップループを高速で回すことが鍵になります。

従来の「コードを書く」作業から「意図を明確にする」作業へとシフトすることで、開発者はより本質的な設計や問題解決に集中できるようになりました。テストファーストの原則と組み合わせることで、品質を保ちながら開発速度を大幅に向上させることができるでしょう。

コメントの質がコード品質を左右するため、明確で構造化されたコメントを書くスキルが今後ますます重要になっていきます。型情報や具体例、アルゴリズムの概要を含めることで、Copilot はより正確なコードを生成してくれますね。

また、生成されたコードを盲目的に受け入れるのではなく、テストを通じて検証する習慣が不可欠です。テスト駆動開発のサイクルは、AI 時代においてもコード品質を担保する最良の手法と言えます。

本記事で紹介した手法を実践することで、皆さんの開発効率が飛躍的に向上することを願っています。

関連リンク