はじめに
こんにちは!
情報科学生兼エンジニア見習いのけんしんです。
みなさん、テスト書いてますか?
僕は書いてません。
テスト書くのめんどくさいですよね。
書いても機能が増えるわけじゃないし、書かなくても動くし、なんか考えること増えるし…
けど、どうもテストがあると便利らしいんです。
なんで今回は僕がテストを書くのではなく、ChatGPTにテストを書いてもらいたいと思います。
ChatGPTがテストを書いてくれるなら多少ラクできますし、人工知能(っぽいもの)がコードを書いてくれるのってロマンがあって良いですよね。
今回は、言語として僕が好き好んで愛用しているGolang、ChatGPTは(苦学生なんで)無料版を使ってやっていきます。
やること
- テスト対象となるコードを作らせてみる
- テストを生成させてみる
- 実際にテストをしてみる
テスト対象となるコードを作らせてみる
テストをするにあたってテスト対象となるコードが必要です。
自分で書いても良いんですが、めんどくさいのでこれもChatGPTにお願いしてみましょう。
とりあえずシンプルなFizzBuzzとかにしてみますか。
(ちなみにFizzBuzzの説明はリンク先参照)
生成してくれたコードが以下です。
package main
import "fmt"
func FizzBuzz(n int) {
for i := 1; i <= n; i++ {
if i%3 == 0 && i%5 == 0 {
fmt.Println("FizzBuzz")
} else if i%3 == 0 {
fmt.Println("Fizz")
} else if i%5 == 0 {
fmt.Println("Buzz")
} else {
fmt.Println(i)
}
}
}
func main() {
FizzBuzz(100) // 引数として任意の数字を指定できます
}
指示は「GolangでFizzBuzzのプログラムを生成してください。」と「上記のプログラムを受け取った引数の数字までループする関数にしてください。」の二つです。
最初はデフォルトで1~100までループする関数になっていたので2個目の指示を行いました。
コードの実行結果
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
ぱっと見コード自体も実行結果も問題なさそうに見えます。
今回はこのコードをテストしてみましょう
テストを生成させてみる
早速ChatGPTにコードを渡してテストを生成してもらいましょう。
指示の文面は以下です。
テストを生成してください。
テスト対象となるコードは以下です。
{{先ほど生成したテスト対象となるコードを入れる}}
結果以下のコードが生成されました。
長いため折り畳み
package main
import "testing"
func TestFizzBuzz(t *testing.T) {
// テストケース1: 15までのFizzBuzz
expected1 := `1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
`
checkFizzBuzz(t, 15, expected1)
// テストケース2: 20までのFizzBuzz
expected2 := `1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
`
checkFizzBuzz(t, 20, expected2)
// テストケース3: 30までのFizzBuzz
expected3 := `1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
`
checkFizzBuzz(t, 30, expected3)
}
func checkFizzBuzz(t *testing.T, n int, expected string) {
t.Helper()
// 出力をキャプチャするためのバッファ
buffer := new(strings.Builder)
// 標準出力をバッファにリダイレクト
stdout = buffer
FizzBuzz(n)
// バッファから出力を取得
got := buffer.String()
if got != expected {
t.Errorf("FizzBuzz(%d)の結果が期待値と異なります。\n期待値:\n%s\n実際の出力:\n%s", n, expected, got)
}
}
func main() {
// FizzBuzz関数のテストを実行
testing.RunTest(TestFizzBuzz, nil)
}
生成してくれたコードですが、実行ができません。
定義されていないstdoutにbufferを代入しようとしていたり…
(コメントと、やろうとしていることが違うような?)
// 標準出力をバッファにリダイレクト
stdout = buffer
main関数の中でtestを実行してしまっていたり…
func main() {
// FizzBuzz関数のテストを実行
testing.RunTest(TestFizzBuzz, nil)
}
ちゃんと動くようになるように指示し直してみましょう。
今回は指示が曖昧だったのも良くないような気がするので、要件を整理しました。
また、コードからテストを生成させるのは本質的で無いような気がするので、関数が実現することを文字起こしして、それをベースにテストを生成してもらいました。
以下指示内容(この指示にたどり着くのに多少の試行錯誤がありました)
以下の要件を満たしたテストコードを生成してください。
テスト要件
- テスト対象はmainパッケージ内にあるFizzBuzz関数である。
- FizzBuzz関数はmain.goというファイルの中に記述されている。
- テストコードはmain_test.goに記述する。
FizzBuzz関数の要件
- 引数としてint型のnを受け取る。
- 返り値を持たない。
- 1からnまでの数字をループで順番に処理をする。
- ループ時、その数字が3で割り切れる場合はFizzという文字列を標準出力する。その数字が5で割り切れる場合はBuzzという文字列を標準出力する。両方の条件を満たす場合はFizzBuzzという文字列を標準出力する。どの条件も満たさない場合は数字そのものを標準出力する。
結果として以下のテストコードが生成されました。
// main_test.go
package main
import (
"bytes"
"io"
"os"
"strings"
"testing"
)
func TestFizzBuzz(t *testing.T) {
// テストケース1: 1から15までのFizzBuzzを検証する
expected := `1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
`
result := captureOutput(func() {
FizzBuzz(15)
})
if result != expected {
t.Errorf("Expected output:\n%s\nBut got:\n%s", expected, result)
}
// テストケース2: 引数が負の場合は何も出力されないことを検証する
result = captureOutput(func() {
FizzBuzz(-5)
})
if result != "" {
t.Errorf("Expected empty output, but got:\n%s", result)
}
// テストケース3: 引数が0の場合は何も出力されないことを検証する
result = captureOutput(func() {
FizzBuzz(0)
})
if result != "" {
t.Errorf("Expected empty output, but got:\n%s", result)
}
}
// 標準出力をキャプチャするヘルパー関数
func captureOutput(f func()) string {
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
return strings.TrimSpace(buf.String())
}
無事、生成ができました。
構文エラーもなさそうです。
内容としては、15と-5と0をそれぞれ引数として渡した時に、期待する出力が標準出力にされているかをチェックするテストコードが生成されているみたいです。
標準出力を扱うため、captureOutput
なんていうヘルパー関数も作ってくれているみたいです。(かしこい)
では、このコードをmain_test.goに配置して、go test
コマンドを実行してテストが通るか試してみましょう。
実際にテストをしてみる
構文的に問題もなく、ぱっと見テストケースも問題なさそうです。(テストケースが十分かはさておき…)
おそらくパスするでしょう。
と思いきや、
❯ go test
--- FAIL: TestFizzBuzz (0.00s)
main_test.go:35: Expected output:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
But got:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
FAIL
exit status 1
FAIL go_test 0.295s
FAILが出てしまいました。
これは実行結果がテストを通らなかったということです。
ChatGPTにコード内容を解説してもらいながら試行錯誤して原因究明。
原因はcaptureOutputにありました。
return時に標準出力の文字列の末尾の空白をstrings.TrimSpaceによってトリムしていたため、テストケースと一致しなかったのです。
return strings.TrimSpace(buf.String())
そこでcaptureOutputのreturn部を以下のように修正しました。
return strings.TrimSpace(buf.String())
その結果無事全てのテストがpassしました。
まとめアンド感想
今回は、ChatGPTを用いてテストを生成してみました。
結果として、無事テストを生成することができました。
これでテスト書かなくて良いんでテスト開発楽チンですね…
ただちゃんと動くテスト生成させるの大変すぎる!!
軽率にテスト生成させると構文エラーがあったり、間違った解釈のテストが生成されます。
それを修正したり、そういった間違いが起きないようにChatGPT伝えるためには、コードやテストに対する理解力と物事を分かりやすく伝えられる要約力が求められる気がします。
それがめちゃくちゃ難しいし大変。
結構時間もかかります。
あと、テストケースの網羅性が低い気がしますね。
もうちょっと網羅的にテストしてくれた方が安心感がある気がします。
結局のところ、ChatGPTで適切で良いテストを生成してもらうためには、適切で良いテストを書ける能力がある程度必要になると感じました。
ただ、僕は今後テストに限らずコードを書く際にChatGPTを活用していきたいですね。
ChatGPTが生成してくれたコードはChatGPTと相談しながら修正することはある程度可能ですし(僕はできた(N=1))、テストの網羅性に関してもどのようなテストが必要かをChatGPTから聞き出してそれをもとに網羅的にしてくれるように指示すればより良いテストを生成してくれるような気がします。
自分でコードを書くより圧倒的に早くコードを書けるし、実装方法がわからない処理も実装してくれますしめちゃ助かります。
(ちゃんと動作するかや、実装の意味はちゃんと調べるべきだけど)
ということで記事は以上です。
みなさんもぜひChatGPT使って良きエンジニアライフを。
では!