🚂

vscode x Copilotを使ってラクで可読性高めなGoのテストを書きたいとき

2025/01/11に公開

早めにテストを用意することで、テスト駆動開発やその後のリファクタが容易なのは理解しつつ、テストパターンの洗い出しや、運用しやすいテストを書くのが少し億劫になることはないでしょうか?
今回は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