サンフランシスコで2024年10月29日から30日にかけて開催されたイベントGitHub Universe 2024に合わせて、さまざまな新技術・新機能に関するリリースが10月29日にGitHub社より発表されました。
とくに、GitHub Copilotがマルチモデル対応しAnthropic社のClaude 3.5 SonnetやGoogle社のJemini 1.5 Proなど最新のLLMが使用可能となったこと、自然言語だけで動作可能なマイクロアプリを構築できるツール「GitHub Spark」はSNS上でも大きな話題となりました。
これらと比べると一見インパクトは薄いものの、VS CodeのGitHub Copilotでマルチファイル編集を可能とする「Copilot Edits」はAIコーディング支援によって開発生産性を飛躍的に向上させることが可能な画期的な機能です。
(生成コードエディタ「Cursor」を使っている方には、「(Cursorの)Composerに相当する機能がいよいよGitHub Copilotでも実装された」と言えば通じるでしょう)。
本記事では、Copilot Edits機能の活用例として人間とコード生成AIによるピンポンプログラミング(ペアプロでテスト駆動開発を行う手法)を題材とし、機能の使い方をご紹介します。なお当該機能は執筆時点でプレビュー版であり、一般公開となる時点ではUIや操作方法、機能が変更される可能性もありますのでご留意ください。
Copilot Editsの基本的な使い方
VS CodeのCopilotメニューから[Open Copilot Edits]を選択すると、Copilot Editsのペインが表示されます。Copilot Editsでは、編集対象の複数のファイルをWorking Setという単位にまとめます。[Add Files...]よりファイルを選択してWorking Setに含めた上で、プロンプトを入力して指示を出しましょう(次の図の赤枠部分)。
図の例では、each.js
ファイル(エディタ上部)に空実装のeach
関数が定義されており、each.test.js
ファイル(エディタ下部)にはeach
関数に対するテストケースが3件記述されています。テスト駆動開発で少しずつ実装を進めていくために、最初のシンプルなテストケースを除き、skip
を指定してテスト実行対象外としています。
※言語はJavaScript、テスティングライブラリはJest、実行環境はNode.jsを利用
コード提案の受け入れ
チャットの応答を確認しましょう。Working Setのファイルのうち、修正提案があるものはボールド表示となっています。ファイル名をクリックするとエディタタブが開き、修正内容を確認できます。差分をプレビュー表示できるので、とてもわかりやすいですね。
提案内容を受け入れる場合は[Accept]ボタンを、破棄する場合は[Discard]ボタンをクリックします。コード断片をコピー&ペーストで貼り付けたり、カーソルを移動して挿入したりといった作業は不要なのでとても楽です。
注意すべき点が二つあります。
一つ目。個々のコード修正提案を受け入れ(または破棄)した後に[Done]ボタンが表示されますが、これをクリックするとWorking Setに対する編集作業が完了という扱いになってしまうので、最後の最後まで押さないようにしてください。
二つ目。コード修正提案の受け入れはメモリ上の操作であり、明示的に保存を行うまで確定されません。
今回の例のようにテスト駆動開発で進める場合、テストコードを実行して関数の動作を検証する必要があるため、忘れずに保存をしなくてはなりません。
ターミナルから npm run test
コマンドを実行してテストを流すと、最初のテストケースがパスし、コード生成AIの提案した実装が正しいことを確認できました。
以下のステップを繰り返して実装を進めていきます。
- 次のテストケースの
skip
属性を外してチャットで実装を指示 - コード生成の修正提案を確認して受け入れ
- テストを実行
全てのテストケースがパスしたら実装完了です!
人間がテストコードを書き、コード生成AIがそれをパスするようなプロダクションコードを書くという一連のやり取り(ピンポンプログラミング)が実現できました。
Tips
Copilot Edits機能を使ってテスト駆動開発を進める上で役立つTipsをいくつかご紹介しましょう。
エラーの原因特定
全てのテストケースが一発でパスするような、幸せな物語ばかりではありません。
無情にもテストが失敗し、エラーが吐かれたときにも、コード生成AIの力を借りてエラー原因の特定とコード修正を行うことが可能です。
その場合、チャット欄に #terminalLastCommand
を付けてエラー調査を依頼すると、Copilotがターミナルの履歴を参照することで適切な回答をしやすくなります。(#terminalLastCommand
はコンテキスト変数と呼ばれるものの一つで、ターミナルにおける直前の実行コマンドとその出力が格納されています)。
コードの説明、リファクタリング
テストをパスする「動作するコード」ができあがったら終わりではありません。「動作するきれいなコード」に仕上げる工程が必要です。
自分が書いたコードではなく他人(コード生成AI)が書いたコードですから、まずは実装を理解しなくてはなりません。コード生成AIに説明させましょう。
このとき、単に説明をさせるのではなく、「具体例を用いて処理過程も示すようにしてください」というプロンプトを添えるとよいでしょう。
改善すべき点が見つかった場合、リファクタリングを指示しましょう。
その他のアップデート
2024年10月29日のGitHub Copilotアップデートには、他にも多くの新機能や機能改善が含まれます。詳しくは公式ブログ記事をご参照ください。
まとめ
この記事では、GitHub Copilot(VS Code版)の新機能であるCopilot Editsを使ったテスト駆動開発をご紹介しました。
Copilot Editsのマルチファイル編集機能は、他にもさまざまな用途で活用できるでしょう。たとえば、メイン関数とメインから呼び出される関数を複数ファイルで構成し、適切に構造分割されたコードを生成させることが可能になります。
私は、コード生成AIに一発で大きなプログラムを生成させることは「ガチャ」だと考えています。プロダクション環境にデプロイするコードの生成を運任せにしてはいけません。コード生成AIが生成するコードの精度と内部品質を向上させる鍵は、問題を小さく分割することです。
この記事で紹介したテクニックやCopilot Editsの機能を使うことで、皆さまのコード生成AIライフが向上することを願っています。
コードサンプル
今回のテスト駆動開発で扱ったサンプルコードを以下に掲載します。
なおこの題材は、以前私がCursor + Claude 3.5 Sonnetで試した内容です(私の個人ブログ記事を参照)。
テストコード(人間が作成):
const { each } = require("../src/each"); describe("each関数のテスト", () => { test("入力が空の場合", () => { const mockCallback = jest.fn(); each``(mockCallback); expect(mockCallback).not.toHaveBeenCalled(); }); test("データ行の数だけコールバックが呼ばれ、引数にはそれぞれのデータが渡される(属性1つ)", () => { const mockCallback = jest.fn(); each` name ${"Alice"} ${"Bob"} ${"Charlie"} `(mockCallback); expect(mockCallback).toHaveBeenCalledTimes(3); expect(mockCallback).toHaveBeenNthCalledWith(1, { name: "Alice", }); expect(mockCallback).toHaveBeenNthCalledWith(2, { name: "Bob", }); expect(mockCallback).toHaveBeenNthCalledWith(3, { name: "Charlie", }); }); test("データ行の数だけコールバックが呼ばれ、引数にはそれぞれのデータが渡される", () => { const mockCallback = jest.fn(); each` name | age | isAdmin ${"Alice"} | ${25} | ${true} ${"Bob"} | ${30} | ${false} ${"Charlie"} | ${35} | ${true} `(mockCallback); expect(mockCallback).toHaveBeenCalledTimes(3); expect(mockCallback).toHaveBeenNthCalledWith(1, { name: "Alice", age: 25, isAdmin: true, }); expect(mockCallback).toHaveBeenNthCalledWith(2, { name: "Bob", age: 30, isAdmin: false, }); expect(mockCallback).toHaveBeenNthCalledWith(3, { name: "Charlie", age: 35, isAdmin: true, }); }); });
プロダクションコード(コード生成AIが生成):
const _ = require('lodash'); function each(strings, ...values) { return (callback) => { const headers = strings[0].trim().split(/\s*\|\s*/); const rows = _.chunk(values, headers.length); _.each(rows, (row) => { const obj = _.zipObject(headers, row); callback(obj); }); }; } module.exports = { each, };
執筆:@tyonekubo、レビュー:@nagamatsu.yuji
(Shodoで執筆されました)