はじめに
本記事では、Google Colab上でGoのコードを動かしながら、go testの基本的な使い方を「テストがOKの場合」「NGの場合」に分けて解説します。
AIによるコード生成、いわゆる「バイブコーディング」が当たり前になったことで、コードを書くこと自体のハードルは大きく下がりました。その一方で、生成されたコードが本当に意図通り動くのかを確かめる「テスト検証」の重要性はむしろ増しています。
Go言語はテスト検証の仕組みが言語仕様レベルで決まっており、テストの手法は迷う余地がなく、AIにとっても明快です。安全なバイブコーディングの手法を考える際の参考にしてください。
学習を進めるために
なお、「Google ColabでGoのプログラムを動かすなんてできるの?」で引っかかってる読者は、まず下記記事で予習してください。
すぐに使えるチートシートはこちら Google Colab版
すぐに実行して、試せるコードレシピはこちら。
Google Colabでのセットアップ:go.mod作成とパッケージ構成
Colabのセルは基本Pythonですが、%%writefileマジックでファイルを書き出し、!でシェルコマンドを実行できます。Goがインストール済みであれば、そのままGoのプロジェクトを組み立てられます。
go.modが存在しないディレクトリではgo test ./...のようなパッケージパターンを解決できず、以下のようなエラーになります。
pattern ./...: directory prefix . does not contain main module or its selected dependencies
そのため、まずはgo mod initでモジュールを作るところから始めます。
!go mod init example
go: creating new go.mod: module example
go: to add module requirements and sums:
go mod tidy
続けてgo mod tidyも実行しておきます(外部依存が無いため今回は特に変化はありません)。
!go mod tidy
go: warning: "all" matched no packages
次に、ディレクトリ構成をpackage mainとヘルパーパッケージ(ライブラリパッケージ)に分けます。実務のGoプロジェクトでは、mainパッケージには起動処理だけを置き、実際のロジックは別パッケージに切り出すのが一般的な作法です。ここではcalcという計算用パッケージをmainとは別ディレクトリに用意します。
example/
├── go.mod
├── main.go
└── calc/
├── calc.go
└── calc_test.go
サブディレクトリを作ってから、それぞれのファイルを書き出します。
!mkdir -p calc
%%writefile calc/calc.go
package calc
func Add(a, b int) int {
return a + b
}
%%writefile calc/calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}
main.goはcalcパッケージをインポートして使うだけのシンプルな形にします。
%%writefile main.go
package main
import (
"fmt"
"example/calc"
)
func main() {
fmt.Println(calc.Add(2, 3))
}
これでgo test ./...を実行すると、モジュール配下のパッケージがすべて自動的に検出され、テストが実行されます。
!go test ./...
? example [no test files]
ok example/calc 0.002s
mainパッケージにはテストファイルがないので[no test files]と表示されますが、calcパッケージのテストはきちんと実行されてokになっています。go test ./...はディレクトリを再帰的に辿ってパッケージ単位でテストを探すため、パッケージを増やすたびにいちいち実行対象を指定し直す必要はありません。
go testの基本ルール
-
go testはカレントディレクトリ配下の*_test.goファイルを自動的に探して実行する - テスト関数は
func TestXxx(t *testing.T)という命名規則に従う必要がある(Xxxは大文字始まり)
テストがOKの場合
先ほどのcalc.Add(2, 3)は5を返すので、テストは成功します。-vを付けると、実行されたテスト名と結果が個別に表示されます。
!go test -v ./calc/...
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example/calc 0.002s
okとパッケージ名、実行時間が出ていればすべて成功です。標準出力に余計なログが出ていなければ、それだけで「壊れていない」ことが確認できます。
テストがNGの場合
期待値をわざと間違えてみます。
%%writefile calc/calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 6 // 本来は5
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}
!go test -v ./calc/...
=== RUN TestAdd
calc_test.go:9: Add(2, 3) = 5; want 6
--- FAIL: TestAdd (0.00s)
FAIL
FAIL example/calc 0.002s
FAIL
失敗時の出力で見るべきポイントは次の3つです。
-
FAILとファイル名・行番号(calc_test.go:9)でどこが落ちたか特定できる - 実際値(got)と期待値(want)がそのままログに出る
- パッケージを指定するパターン(
./calc/...)でテストした場合、最後にもう一度FAILが表示され、全体としてテストが失敗したことが一目でわかる。プロセスの終了コードも非ゼロになるため、CIなどでもそのまま失敗として検知できる
t.Errorとt.Fatalの違い
どちらも「テスト失敗」を報告しますが、動きが異なります。
| メソッド | 失敗後の挙動 |
|---|---|
t.Error / t.Errorf
|
失敗を記録しつつ、そのテスト関数の残りの処理を継続する |
t.Fatal / t.Fatalf
|
失敗を記録し、その場でテスト関数を中断する(goroutineを止める) |
たとえば事前準備(DB接続など)が失敗した時点でそれ以降の検証に意味がない場合はt.Fatal、複数の項目を一度にチェックして全部の失敗箇所をまとめて知りたい場合はt.Error、という使い分けが基本です。
サブテストとテーブル駆動テスト
Goのテストでは、複数のケースを1つの関数にまとめる「テーブル駆動テスト(table driven test)」がよく使われます。calc_test.goを元の内容に戻しつつ、テーブル駆動の形にしてみます。
%%writefile calc/calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b, want int
}{
{"正の数同士", 2, 3, 5},
{"負の数を含む", -1, 1, 0},
{"ゼロ", 0, 0, 0},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
got := Add(c.a, c.b)
if got != c.want {
t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want)
}
})
}
}
!go test -v ./calc/...
=== RUN TestAdd
=== RUN TestAdd/正の数同士
=== RUN TestAdd/負の数を含む
=== RUN TestAdd/ゼロ
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/正の数同士 (0.00s)
--- PASS: TestAdd/負の数を含む (0.00s)
--- PASS: TestAdd/ゼロ (0.00s)
PASS
ok example/calc 0.002s
t.Runでサブテストにすると、-v実行時にケースごとの名前とPASS/FAILが個別に表示され、-run TestAdd/負の数を含むのようにケース単位で絞り込んで実行することもできます。
package mainから切り出したヘルパー関数のテストもお手のもの
ここまでの構成を振り返ると、main.go自体はcalc.Addを呼び出すだけで、テストはcalcパッケージ側にしか書いていません。これがGoにおけるテスト設計の基本パターンです。
-
mainパッケージは起動処理(フラグ解析、DI、main()の呼び出しなど)に専念させ、テストしにくい・書く必要が薄い部分を最小限にする - 実際のロジック(ヘルパー関数)は別ディレクトリのパッケージに切り出す
- 切り出したパッケージは
mainから独立してimportできるので、単体テストは通常の関数テストとまったく同じ書き方で済む
この構成のまま、たとえばcalcとは別にstrutilのような文字列処理パッケージを追加しても、go test ./...は自動的にそれも拾って実行してくれます。実際にやってみます。
!mkdir -p strutil
%%writefile strutil/strutil.go
package strutil
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
%%writefile strutil/strutil_test.go
package strutil
import "testing"
func TestReverse(t *testing.T) {
got := Reverse("hello")
want := "olleh"
if got != want {
t.Errorf("Reverse(\"hello\") = %q; want %q", got, want)
}
}
!go test ./...
? example [no test files]
ok example/calc 0.010s
ok example/strutil 0.004s
example/
├── go.mod
├── main.go
├── calc/
│ ├── calc.go
│ └── calc_test.go
└── strutil/
├── strutil.go
└── strutil_test.go
パッケージが増えても、実行対象を個別に指定し直す必要はなく、常にgo test ./...一発でモジュール全体の検証ができます。「ロジックをmainから切り離してパッケージ化する」「そのパッケージを単体テストする」という流れが言語標準のツールだけで完結するのは、Goの設計として大きな強みだと思います。
Goのテストの「お作法」が確立している魅力
Goの大きな特徴の一つは、テストの書き方・回し方がほぼ一択に定まっている点だと思います。
- テストは
testingパッケージという標準ライブラリだけで完結し、外部フレームワークの導入検討が基本的に不要 - ファイル名は必ず
_test.go、関数名は必ずTestXxxという命名規則が言語仕様レベルで固定されている - 実行コマンドは常に
go test一本で、設定ファイルやプラグイン選定に悩む余地が少ない - パッケージ単位でテストが完結するため、
mainとロジックを分ける設計が自然にテストしやすい構成へつながる
言語によっては、「このプロジェクトはどのテスト作法を採用しているか」を都度確認することが、新人にとって大きな負担となる場合もあります。しかしGo言語は、統一されたテスト検証の手法があるため、チーム開発のルールを揃える手間を大きく減らしてくれそうです。Goではgofmtがコードの書式を統一するのと同じ思想で、テストの書き方自体も統一し、他人のコードのテストを読む・書くハードルが低く保つことが、言語仕様レベルで担保されているのでしょう。
バイブコーディング時代こそテストコードをAIで量産する
近年、AIを使ったバイブコーディングによって、プロダクションコードを書くコスト自体は大きく下がっています。しかしコードが増えるほど、それが正しく動くことを保証する仕組みも同じペースで増やす必要があります。ここでこそAIを活用すべきだと思います。
Goのテストはケース1つ1つが「入力・期待値・比較」というシンプルな構造に落とし込みやすく、テーブル駆動テストの形式もパターン化されているため、AIにテストケースを大量に生成させても人間がレビューしやすいという相性の良さがあります。
「実装をAIに書かせたら、そのテストコードもAIに書かせて数を増やす」という運用は、バイブコーディング時代の検証プロセスとして十分に理にかなったアプローチだと考えられます。
弊社について
本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。
主な領域は、
- データマート設計・データパイプライン構築
- SQL / Python を用いたデータ処理設計
- BI導入支援・分析基盤の整備
- 既存システムの運用改善・可視化支援
といった、「データを使える状態にする」ための活動です。
弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。