vscode x Copilotを使ってラクで可読性高めなGoのテストを書きたいとき
早めにテストを用意することで、テスト駆動開発やその後のリファクタが容易なのは理解しつつ、テストパターンの洗い出しや、運用しやすいテストを書くのが少し億劫になることはないでしょうか?
今回はvscode x Copilotを使ってラクにテストを書けるようにしてみました。
以下の運賃判定プログラムのテストを書きたいとします。
ソースコードはこちら
package station
import "errors"
var distances = map[string]int{
"Tokyo": 0,
"Shinjuku": 5,
"Yokohama": 30,
"Nagoya": 350,
"Osaka": 550,
}
const baseFarePerKm = 10
func CalculateFare(startStation, endStation string) (int, error) {
startDistance, ok1 := distances[startStation]
endDistance, ok2 := distances[endStation]
if !ok1 || !ok2 {
return 0, errors.New("指定された駅が存在しません")
}
distance := abs(endDistance - startDistance)
return distance * baseFarePerKm, nil
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
まずはCopilotのGenerate tests
でテストを生成します。
calc_test.go
package station
import (
"testing"
)
func TestCalculateFare(t *testing.T) {
tests := []struct {
startStation string
endStation string
expectedFare int
expectError bool
}{
{"Tokyo", "Shinjuku", 50, false},
{"Tokyo", "Yokohama", 300, false},
{"Nagoya", "Osaka", 2000, false},
{"Shinjuku", "Nagoya", 3450, false},
{"Tokyo", "Unknown", 0, true},
{"Unknown", "Osaka", 0, true},
}
for _, test := range tests {
fare, err := CalculateFare(test.startStation, test.endStation)
if test.expectError {
if err == nil {
t.Errorf("expected error for stations %s to %s, but got none", test.startStation, test.endStation)
}
} else {
if err != nil {
t.Errorf("did not expect error for stations %s to %s, but got %v", test.startStation, test.endStation, err)
}
if fare != test.expectedFare {
t.Errorf("expected fare %d for stations %s to %s, but got %d", test.expectedFare, test.startStation, test.endStation, fare)
}
}
}
}
いい感じにテーブル駆動テスト(TDT)使ったテストを書いてくれました。
個人的には、
① 失敗時のテストが特定しにくいので、startStationの前にnameフィールドを追加してほしい
② assert判定が愚直なので、外部のutilライブラリを使って可読性を高めたい
が気になりました。
vscodeはMacだと、cmd+iでインラインでAsk Copilotすることができますので、それぞれの課題をaskして修正してもらいたいとします。
①の対応
失敗時のテストが特定しにくいので、startStationの前にnameフィールドを追加してほしい。nameの値は日本語でわかりやすく書いて
と、インラインで指示するといい感じに修正してくれました。
どこのテストが失敗したのかわかりやすくなりました。
$ go test ./...
? example [no test files]
--- FAIL: TestCalculateFare (0.00s)
--- FAIL: TestCalculateFare/名古屋から大阪 (0.00s)
calc_test.go:35: expected fare 999999 for stations Nagoya to Osaka, but got 2000
FAIL
FAIL example/station 0.308s
FAIL
②の対応
今回は、go-cmpとstretchr/testifyを使ってもらうことにします。
assertでネストを減らせました。
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fare, err := CalculateFare(test.startStation, test.endStation)
if test.expectError {
assert.Error(t, err, "expected error for stations %s to %s, but got none", test.startStation, test.endStation)
} else {
assert.NoError(t, err, "did not expect error for stations %s to %s, but got %v", test.startStation, test.endStation, err)
assert.True(t, cmp.Equal(test.expectedFare, fare), "expected fare %d for stations %s to %s, but got %d, diff: %s", test.expectedFare, test.startStation, test.endStation, fare, cmp.Diff(test.expectedFare, fare))
}
})
}
テスト失敗時は、go-cmpによって直感的になりました。
$ go test ./...
? example [no test files]
--- FAIL: TestCalculateFare (0.00s)
--- FAIL: TestCalculateFare/名古屋から大阪 (0.00s)
calc_test.go:34: expected fare 999999 for stations Nagoya to Osaka, but got 2000, diff: int(
- 999999,
+ 2000,
)
FAIL
FAIL example/station 0.342s
FAIL
まとめ
Copilotは実装の助けにもなりますが、面倒になりがちなパターンの洗い出しやテストの生成もお手の物だと思います。
これから立ち上げるプロジェクトのときにも積極的に使っていきたいと思いました。
ありがとうございました!
Discussion