GitHub CopilotとChatGPTに関数の作成からテストの作成までのすべてを任せてみた

# はじめに

最近は、もっぱらAI駆動開発を推し進めるために、いかに説明的な英語をコメントとして残せるかに重きを置いています。

https://github.com/features/copilot

まだまだ発展途上な段階ですが、ここいらで忘備録として一連の生成までの流れを残しておこうと思い、今回記事を書くことにしました。

# ChatGPTにCodexにとって良いコメントの書き方を聞いてみる

まずは悩んでいても仕方がないので、ChatGPTに対して軽く質問をしてみたいと思います。

例えば、`save()`メソッドがあるとして、そのメソッドが呼び出される際に一緒に呼び出されることを目的とした関数を考えてみることにします。

条件としては、次の通りです。

1. save()メソッドが呼び出されたときに呼び出されるメソッドを作成したい。
2. しかし、あなたはパラメータと戻り値の型について何も考えていない

これらの条件に基づいて、質問をしてみました。

```
write appropriate comment as a text prompt for codex

situation:
1. you want to create a method that will be called when save() method is called
2. but you don't have any idea of parameter and return type
```

![名称未設定のデザイン (75).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/f5dacd28-d1ff-2b15-a954-39a3ce3f32bb.png)

すると、次の内容が答えとして返ってきました。

```js
// TODO: Implement a method that will be called when the save() method is called.
// At this moment, we are not sure about the parameters and return type needed for this method.
// Please determine the required parameters and return type before implementing the method.
```

今度は、**より良い例**という注釈をつけて再度質問してみました。

```
show good examples of how to write a comment as a text prompt for codex model when you use GitHub Copilot
```

![名称未設定のデザイン (76).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/26d19638-6564-896b-c7ff-d6978d5257c8.png)

そして、次のような答えが返ってきました。

```js
// TODO: Write a function to calculate the factorial of a given positive integer.
// The function should accept a single integer as its argument and return the factorial value as an integer.
// Example usage: factorial(5) should return 120.
```

これらの内容から、ChatGPTが考えるGitHub Copilotにとって理解しやすいコメントのパターンが浮かび上がってきました。

まずは、1行目です。
ここでは、出出しが`TODO:`から始まっています。そして、それに続く文として`Write a function...`となっています。
このことから、`TODO: `などのラベリングのあとに、命令形に続く形で抽象的な関数の内容を交えるといったフォーマットになっていることがみてわかります。

```
ラベル: 命令形 + 命令の対象 + その命令の対象の概要
```

例えば、0から100までの間に存在するランダムな値を生成する関数を作成したい場合は次のようになります。

```
// TODO: Write a function to generate a random integer between 0 and 100
```

もし、APIエンドポイントからデータを取得するメソッドをクラスに実装したい場合は次のようになります。

```
// TODO: Write a method to fetch data from an API endpoint
```

続いて2行目です。
ここでは、どのような引数を受け取りどのような値を返すかを、1行目と比べてより具体的に明示しています。

```
命令の対象 + `should accept` + 引数についての説明 + `and return` 返り値についての説明
```

例えば、先ほどのランダムな値を生成する1行目の内容に続く場合は次のようになります。

```js
// The function should accept no arguments and return an integer
```

APIエンドポイントからデータを取得するメソッドの１行目のコメントの内容につづける場合は、次のようになります。

```js
// The method should accept a single string argument and return a Promise that resolves to a string
```

最後に３行目です。
ここでは、その関数の使い方について具体的な引数を交えて説明しています。

この時に使うラベルは、`Example Usage:`となっています。

```
Example usage: 対象の関数名(具体的な引数) + `should return` + 想定している具体的な返り値.
```

実際の例を続けてみていきましょう。

```js
// Example usage: randomInteger() should return a random integer between 0 and 100
---
// Example usage: fetchData("https://example.com/api") should return a Promise that resolves to a string
```

最後に、通しでみていきましょう。

まずは、0から100までの間に存在するランダムな値を生成する関数を作成したい場合です。

```js
// TODO: Write a function to generate a random integer between 0 and 100
// The function should accept no arguments and return an integer
// Example usage: randomInteger() should return a random integer between 0 and 100
```

次に、エンドポイントからデータを取得するメソッドを作成したい場合です。


```js
// TODO: Write a method to fetch data from an API endpoint
// The method should accept a single string argument and return a Promise that resolves to a string
// Example usage: fetchData("https://example.com/api") should return a Promise that resolves to a string
```

# GitHub Copilotにコメントを通してコードを生成させてみる

では、先ほど使用した例を用いて、実際にGitHub Copilotに指示を出していきたいと思います。

今回作成したい関数は、0から100までの間に存在するランダムな値を生成する関数です。

```js
// TODO: Write a function to generate a random integer between 0 and 100
// The function should accept no arguments and return an integer
// Example usage: randomInteger() should return a random integer between 0 and 100
```

GitHub Copilotは次のコードを生成しました。

```js
function randomInteger() {
  return Math.floor(Math.random() * 101);
}
```

:::note warn
もし、コメントを書いたにも関わらずコードを生成してくれない場合は、GitHub Copilotの背中を押してあげる必要があります。この場合、`function`と出だしを書いてあげることで、コンテキストから推論して関数名から最後のとじカッコまでを補完してくれます。
:::

# 生成されたコードに対してテストを作成してもらう

それでは、先ほど生成された`randomInteger()`対して、ユニットテスを作成していきましょう。

まずは、GitHub Copilotを使ってテストを作成してもらうパターンを見ていきます。

GitHub Copilotではリアルタイムの推論を行うためにOpenAIの`Codex`が採用されています。

https://github.com/features/copilot

Codexに代表されるようなコードを補完してくれたり生成したりしてくれるモデルには、コンテキストがとても大事となってきます。

そのため、テストに使用したいライブラリが`mocha`や`sinon`の場合はファイルの一番上でインポート文を書いてあげるだけで、コメントを使って明示的に説明しなくても理解してもらうことができます。

そして、テスト対象となる`randomInteger`もインポートしておくことで、コンテキストに含めてテストを設計してくれます。

```js
import * as assert from "assert";
import { randomInteger } from "~/randomInteger"
```

今回も、ChatGPTにテストを作成する場合の良いコメントの作成方法を聞いてみました。

```
show good example to write comment for codex model when you use GitHub Copilot when you want to write a test in TypeScript
```

すると、次の答えが返ってきました。

```
// TODO: Write a test suite in TypeScript for a function called 'myFunction'.
// The 'myFunction' takes two arguments: a string and an array of strings.
// It returns an array of strings that contains the elements from the input array that start with the input string.
// Please ensure proper test coverage by including positive, negative, and edge cases.
// Example usage: myFunction('app', ['apple', 'banana', 'application']) should return ['apple', 'application'].
```

この内容をもとに、先ほどの`randomInteger()`に合わせたコメントに修正します。

```js
// TODO: Write a test suite in TypeScript for function called `randomInteger`.
// The `randomInteger` function takes no arguments and returns a random integer between 0 and 100.
// Please ensure proper test coverage by including positive, negative, and edge cases.
// Example usage: randomInteger() should return a random integer between 0 and 100.
```

そして、上記のコメントをもとに、GitHub Copilotにテストの作成をしてもらいました。

```js
import * as assert from "assert";
import { randomInteger } from "~/randomInteger"

// TODO: Write a test suite in TypeScript for function called `randomInteger`.
// The `randomInteger` function takes no arguments and returns a random integer between 0 and 100.
// Please ensure proper test coverage by including positive, negative, and edge cases.
// Example usage: randomInteger() should return a random integer between 0 and 100.
describe("randomInteger", () => {
  it("should return a random integer between 0 and 100", () => {
    const randomNumber = randomInteger();
    assert.ok(randomNumber >= 0 && randomNumber <= 100);
  });
});
```

実際にテストを実行したところ、きちんと通ることが確認できました。

```bash
mocha --require ts-node/register ./example.test.ts

  randomInteger
    ✔ should return a random integer between 0 and 100

  1 passing (2ms)
```

# おまけ

今度は、GitHub Copilotに頼らず、ChatGPT自体を使って、テストコードを生成してもらいました。

今回使用したのは、こちらのVSCode拡張です。

https://marketplace.visualstudio.com/items?itemName=takagimeow.chatgpt-editor

すると、今度は全く違ったテストコードが返ってきました。

```js
describe("randomInteger", () => {
  it("should return a number between 0 and 100", () => {
    const result = randomInteger();
    assert.ok(result >= 0 && result <= 100);
  });

  it("should return an integer", () => {
    const result = randomInteger();
    assert.strictEqual(typeof result, "number");
    assert.strictEqual(result % 1, 0);
  });

  it("should return different numbers on subsequent calls", () => {
    const result1 = randomInteger();
    const result2 = randomInteger();
    assert.notStrictEqual(result1, result2);
  });

  it("should return 0 when Math.random() returns 0", () => {
    const mockMath = Object.create(global.Math);
    mockMath.random = () => 0;
    global.Math = mockMath;
    const result = randomInteger();
    assert.strictEqual(result, 0);
  });

  it("should return 100 when Math.random() returns 0.999999...", () => {
    const mockMath = Object.create(global.Math);
    mockMath.random = () => 0.9999999999999999;
    global.Math = mockMath;
    const result = randomInteger();
    assert.strictEqual(result, 100);
  });
});
```

GitHub Copilotでは1つのテストケースしか生成してくれませんでしたが、ChatGPTに頼むと５つのテストケースを作成してくれました。

そして、テストが通ることも確認できました。

```bash
> mocha --require ts-node/register ./example.test.ts

  randomInteger
    ✔ should return a number between 0 and 100
    ✔ should return an integer
    ✔ should return different numbers on subsequent calls
    ✔ should return 0 when Math.random() returns 0
    ✔ should return 100 when Math.random() returns 0.999999...

  5 passing (3ms)
```

今回使用したモデルは、`gpt-3.5-turbo-0301`です。

GitHub Copilotに搭載されているモデルは、Codexと呼ばれるGPT-3をチューニングした特別なモデルとなっています。

そのため、もうすぐリリース予定のGitHub Copilot Xでは、GPT-4モデルが採用されるとアナウンスされていますから、より精度の高いテストやコードの生成が可能になることが上記の結果から予想されます。

# まとめ

GitHub Copilotに対しては賛否両論がありますが、今回の例をみていただいてわかるとおり、適切なプロンプティングと必要なコンテキストさえ与えてあげれば、とてつもない威力を発揮してくれることが証明でできたと思います。

これから先、プロンプティングが必ず必要な技能となってきます。
ビルゲイツも、この動画の中で、`The description in English will be the program.`と語っていました。

https://www.youtube.com/watch?v=bHb_eG46v2c&feature=youtu.be

適切なプロンプティングとは、すなわち読みやすく理解しやすい説明ということですから、エンジニアのコーディング技術の向上につながってくれると僕は信じています。

# 参考にした記事

https://ascii.jp/elem/000/004/094/4094837/

https://ascii.jp/elem/000/004/129/4129658/

https://microsoft.github.io/prompt-engineering/

https://shift.infinite.red/getting-the-most-from-github-copilot-8f7b32014748

https://github.com/jamonholmgren/react-native-colo-loco/blob/ecfd0ff3db4af75cbb284fcfd572e75c5038435c/scripts/android.groovy

https://www.strictmode.io/articles/using-github-copilot-for-testing

https://safjan.com/how-to-get-most-of-github-copilot/

https://platform.openai.com/docs/guides/code/introduction

https://www.builder.io/blog/vscode-tips

https://realpython.com/github-copilot-python/