Testing - Golang learning step 9-1
- 公開日
- カテゴリ:TestingYourApps
- タグ:Golang,roadmap.sh,学習メモ

roadmap.sh > Go > Testing Your Apps の学習を進めていきます。
※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。
contents
- 開発環境
- 参考 URL
- 標準 testing パッケージ
- 基本的なテストの書き方
- t.Errorf と t.Fatal
- テストケースのパラメータ化
- サブテスト
- ベンチマークテスト
- カバレッジ測定
- testing.T の便利なメソッド
- TestMain を使ったセットアップとクリーンアップ
- *_test.go 以外のコードからテスト実行
- assert() を使った Go のテスト方法
開発環境
- チップ: Apple M2 Pro
- OS: macOS Sonoma
- go version: go1.23.2 darwin/arm64
参考 URL
- [official] Go Tutorial: Add a Test
- [article] Go by Example: Testing
- [article] YourBasic Go: Table-driven unit tests
- [article] Learn Go with Tests
- [feed] Explore top posts about Testing
標準 testing パッケージ
Go には、標準でテストを実行するための testing パッケージが用意されています。この testing パッケージを使うことで、ユニットテストやベンチマークテストを簡単に記述・実行できます。
基本的なテストの書き方
Go では、テストコードを *_test.go
というファイル名で作成し、テスト関数は func TestXxx(t *testing.T) {}
の形式で定義します。
add.go
package main
func Add(a, b int) int {
return a + b
}
add_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
作成したテストは、次のコマンドで実行できます。
go test
PASS
ok testing 0.348s
詳細な出力を見たい場合は -v オプションを付けます。
go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok testing 0.306s
t.Errorf と t.Fatal
testing.T
型の t
を使って、テストの失敗を報告できます。
t.Errorf(format, args...)
: エラーメッセージを出力してテストを続行するt.Fatal(args...)
: エラーメッセージを出力し、即座にテストを中断する
func TestExample(t *testing.T) {
t.Errorf("This is an error, but the test continues")
t.Fatal("This is a fatal error, test stops here")
}
テストケースのパラメータ化
複数の入力値でテストを実施する場合、テーブル駆動テスト(table-driven test)がよく使われます。
func TestAddTableDriven(t *testing.T) {
tests := []struct {
a, b int
expected int
}{
{1, 1, 2},
{2, 3, 5},
{-1, 1, 0},
}
for _, tt := range tests {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
}
}
サブテスト
t.Run
を使うことで、個別のテストケースをサブテストとして実行できます。
func TestAddSubTests(t *testing.T) {
tests := map[string]struct {
a, b int
expected int
}{
"positive numbers": {2, 3, 5},
"negative numbers": {-1, -1, -2},
"mixed numbers": {-1, 1, 0},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
ベンチマークテスト
testing
パッケージでは、ベンチマークテストも可能です。func BenchmarkXxx(b *testing.B) {}
形式で関数を作成し、b.N
回のループ内で処理を実行します。
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
ベンチマークは以下のコマンドで実行します。
go test -bench .
goos: darwin
goarch: arm64
pkg: testing
cpu: Apple M2 Pro
BenchmarkAdd-10 1000000000 0.2937 ns/op
PASS
ok testing 0.548s
goos
, goarch
, cpu
についてはマシンのスペックです。テストについては以下
BenchmarkAdd-10 1000000000 0.2937 ns/op
これは、BenchmarkAdd
というベンチマーク関数の測定結果を示しており、以下の情報が含まれています。
BenchmarkAdd-10
:BenchmarkAdd
は、テストした関数名 (Add
関数) に基づいています。-10
の部分は、Go のベンチマークが並列実行されるスレッドの数を示します。この値は Go のtesting
パッケージが自動で決定します(CPU コア数、正確にはGOMAXPROCSの値に応じて変化することが多い)。
1000000000
:b.N
の値を表しており、ベンチマーク関数が 10 億 (1,000,000,000
) 回実行されたことを意味します。- Go のベンチマークは、安定した測定ができるまで
b.N
の値を調整しながら関数を繰り返し実行します。
0.2937 ns/op
:Add
関数の 1 回の実行にかかった平均時間が 0.2937 ナノ秒 (ns) であることを示しています。- この値が小さいほど、関数の実行速度が速いことを意味します。
カバレッジ測定
テストカバレッジを測定するには、以下のコマンドを使います。
go test -cover
PASS
coverage: 100.0% of statements
ok testing 0.221s
カバレッジの詳細をファイル単位で確認する場合は、coverprofile
オプションを使用します。
# テスト実行
go test -coverprofile=coverage.out
# カバレッジを HTML で確認(ブラウザが開く)
go tool cover -html=coverage.out
これにより、ブラウザでカバレッジの可視化ができます。
testing.T の便利なメソッド
testing.T
にはテストを支援するさまざまなメソッドがあります。
メソッド | 説明 |
---|---|
t.Error(args...) | エラーメッセージを出力してテストを継続 |
t.Errorf(format, args...) | t.Error のフォーマット版 |
t.Fatal(args...) | エラーメッセージを出力してテストを中断 |
t.Fatalf(format, args...) | t.Fatal のフォーマット版 |
t.Helper() | 呼び出し元をレポートから省略(ヘルパー関数向け) |
t.Skip(args...) | テストをスキップ |
t.Skipf(format, args...) | t.Skip のフォーマット版 |
t.Helper() を使ったヘルパー関数の作成
testing.T
には t.Helper()
というメソッドがあり、テスト内でヘルパー関数(共通のチェック処理など)を定義するときに役立ちます。
t.Helper() の役割
通常、Go のテストではエラーが発生すると t.Errorf()
や t.Fatal()
によってエラーメッセージが出力されますが、エラーメッセージに表示される行番号は、エラーが発生した関数の行になります。
このため、ヘルパー関数内で t.Errorf()
を呼び出した場合、その関数の行番号が表示されてしまい、どこで問題が発生したのか特定しにくくなります。
そこで t.Helper()
を使うと、エラー発生時に ヘルパー関数をスキップして、呼び出し元の行番号をエラーメッセージに表示する ことができます。
t.Helper() を使わない場合
func assertEqual(t *testing.T, expected, actual int) {
if expected != actual {
t.Errorf("expected %d, but got %d", expected, actual)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, 5, Add(2, 3))
}
このコードを実行すると、エラーが発生した場合、assertEqual
関数内の t.Errorf()
の行番号が出力されてしまいます。この場合、TestAdd
のどこで失敗したのかすぐには分かりません。
--- FAIL: TestAdd (0.00s)
add_test.go:5: expected 5, but got 4
t.Helper() を使った場合
t.Helper()
を使うと、エラーが発生したときの行番号が ヘルパー関数内ではなく、実際に assertEqual
を呼び出したテスト関数の行 として表示されるようになります。
func assertEqual(t *testing.T, expected, actual int) {
t.Helper() // 呼び出し元の行番号をエラーメッセージに表示する
if expected != actual {
t.Errorf("expected %d, but got %d", expected, actual)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, 5, Add(2, 3))
}
このように t.Helper()
を追加すると、エラーの発生元が assertEqual
ではなく、TestAdd
の行として表示されます。
--- FAIL: TestAdd (0.00s)
add_test.go:10: expected 5, but got 4
このように、エラーメッセージの行番号が TestAdd
の行になり、テストケースのどこで失敗したのかが分かりやすくなります。
t.Helper() を使うべき場面
- アサート関数(例:
assertEqual
,assertContains
など) - 共通のセットアップ処理
- 複数のテストケースで使い回すチェック関数
- ログ出力を行うヘルパー関数
t.Helper()
を適切に使うことで、テストのデバッグがしやすくなり、どこで失敗したのかが明確になります。特に assert
系の関数を定義する場合は、必ず t.Helper()
を追加するとよいでしょう。
TestMain を使ったセットアップとクリーンアップ
Go の testing パッケージでは、TestMain(m *testing.M)
を *_test.go
に記述することで、すべてのテストの 前後 に共通のセットアップ(準備)とクリーンアップ(後処理)を行うことができます。
通常の TestXxx(t *testing.T)
関数では個々のテストの準備しかできませんが、TestMain
を使うと テスト全体のセットアップ が可能になります。
TestMain の基本構造
TestMain
は以下のような構造になっています。
func TestMain(m *testing.M) {
// ① セットアップ処理(テストの前に実行)
setup()
// ② すべてのテストを実行
exitCode := m.Run()
// ③ クリーンアップ処理(テストの後に実行)
cleanup()
// 終了コードを返す
os.Exit(exitCode)
}
setup()
でテスト全体に必要な準備を行う(例: データベース接続、モックサーバー起動)。m.Run()
で通常のTestXxx(t *testing.T)
をすべて実行。cleanup()
でテスト終了後にリソースを解放(例: DB のクローズ、環境変数のリセット)。os.Exit(exitCode)
で適切な終了コードを返す。
TestMain の具体例
以下、add_test.go
に TestMain
を記述し、テストの前後でセットアップ (testSetup の初期化) とクリーンアップ (testSetup のリセット) を行っています。
package mathutil
import (
"fmt"
"os"
"testing"
)
var testSetup string // グローバル変数を使ってセットアップ情報を保持
func setup() {
fmt.Println("=== セットアップ実行 ===")
globalResource = "テストリソース準備完了"
}
func cleanup() {
fmt.Println("=== クリーンアップ実行 ===")
globalResource = ""
}
// TestMain はすべてのテストの前後に実行される
func TestMain(m *testing.M) {
setup()
// すべてのテストを実行
exitCode := m.Run()
cleanup()
// テストの終了コードを返す
os.Exit(exitCode)
}
// `TestMain` でセットアップした値をテスト内で利用できる
func TestAdd(t *testing.T) {
if testSetup == "" {
t.Fatal("テストのセットアップが正しく実行されていません")
}
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
} else {
t.Log("TestAdd PASSED")
}
}
テストを実行すると、TestMain の セットアップ → テスト実行 → クリーンアップ の流れで進みます。
go test ./... -v
=== テストのセットアップ開始 ===
=== RUN TestAdd
add_test.go:29: TestAdd PASSED
--- PASS: TestAdd (0.00s)
=== テストのクリーンアップ開始 ===
PASS
ok example.com/mathutil 0.123s
TestMain を使うメリット
メリット | 具体例 |
---|---|
テスト前にデータを準備できる | テスト用データベースを接続する |
すべてのテストが終わった後に後処理ができる | データベース接続を閉じる、ファイルを削除 |
環境変数や設定を一時的に変更し、元に戻せる | os.Setenv("ENV", "test") の変更 |
テスト全体で共通のセットアップを行える | 1回だけ初期化処理をすればよい |
TestMain の主な用途
使うべき場面 | 例 |
---|---|
データベースのセットアップ | testDB = connectToTestDB() |
モックサーバーの起動・停止 | mockServer.Start() → mockServer.Stop() |
一時ファイルの作成・削除 | tmpFile, _ := ioutil.TempFile() |
環境変数の変更・復元 | os.Setenv("ENV", "test") |
*_test.go 以外のコードからテスト実行
通常、Go のテストコードは *_test.go
ファイル内に記述し、go test コマンドを実行することで自動的に testing パッケージがテスト関数を検出して実行します。しかし、*_test.go
以外のファイル(例えば main.go など)から明示的にテストを実行したい場合は、testing パッケージの testing.M
を使います。
go run
コマンドでテストを実行したい場合- CI/CD や特定の環境でカスタムセットアップ後にテストを走らせたい場合
- ベンチマークやテストをアプリケーションの一部として組み込みたい場合
mathutil/main.go
package mathutil
// Add は2つの整数の合計を返す
func Add(a, b int) int {
return a + b
}
mathutil_test.go
(通常のテスト)
通常の testing パッケージを使用したテストを記述します。通常、go test
を実行するとこの TestAdd が走ります。
package mathutil
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
main.go
(テストを main
関数から実行)
ここで、main.go
から testing.M
を使ってテストを手動で実行します。
package main
import (
"fmt"
"github.com/myapp/mathutil"
)
func runTests() {
fmt.Println("手動テスト実行開始")
tests := []struct {
a, b int
expected int
}{
{1, 1, 2},
{2, 3, 5},
{-1, 1, 0},
}
for _, tt := range tests {
result := mathutil.Add(tt.a, tt.b)
if result != tt.expected {
fmt.Printf("Test failed: Add(%d, %d) = %d; want %d\n", tt.a, tt.b, result, tt.expected)
} else {
fmt.Printf("Test passed: Add(%d, %d) = %d\n", tt.a, tt.b, result)
}
}
fmt.Println("手動テスト実行完了")
}
func main() {
fmt.Println("通常のアプリケーションの実行")
fmt.Println("2 + 3 =", mathutil.Add(2, 3))
runTests()
}
実行方法と出力
1. 通常の go test 実行
go test -v
出力:
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example.com/mathutil 0.123s
2. go run main.go
実行
go run main.go
通常のアプリケーションの実行
2 + 3 = 5
手動テスト実行開始
Test passed: Add(1, 1) = 2
Test passed: Add(2, 3) = 5
Test passed: Add(-1, 1) = 0
手動テスト実行完了
この方法では go test
とは異なり、テストの詳細な出力が得られないため、実際のテスト結果を表示するには m.Run()
の戻り値や testing.Verbose()
などを活用する必要があります。
方法 | 特徴 |
---|---|
*_test.go に記述して go test で実行 | 一般的な方法、Go の testing パッケージが自動検出 |
TestMain(m *testing.M) を *_test.go に記述 | テスト前後にセットアップ・クリーンアップが可能 |
main.go から TestMain を実行 | go run でテストを実行、カスタムロジックが可能 |
通常は go test
を使うのが推奨されますが、特定の条件下でテストを手動実行したい場合やセットアップ・クリーンアップが必要な場合には、TestMain(m *testing.M)
を活用できます。
assert() を使った Go のテスト方法
Go の標準 testing パッケージには assert()
のような関数は含まれていませんが、外部ライブラリを使用することで アサーション(assertion)ベースのテスト を簡単に書くことができます。
Go で assert()
を使ってテストを実施する場合、一般的に github.com/stretchr/testify/assert
パッケージを使用します。
testify/assert のインストール
Go モジュールを利用して testify
をインストールします。
go get github.com/stretchr/testify/assert
assert を使ったテストの書き方
assert.Equal()
などを使うことで、テストの可読性を向上できます。
add_test.go
package mathutil
import (
"testing"
"github.com/stretchr/testify/assert"
)
// assert を使ったテスト
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
// アサーションを使用
assert.Equal(t, expected, result, "Add(2, 3) should return 5")
}
エラー発生時の出力
もし Add(2, 3)
が 5
を返さなかった場合、以下のようなエラーメッセージが出力されます。
=== RUN TestAdd
add_test.go:12:
Error Trace: add_test.go:12
Error: Not equal:
expected: 5
actual : 4
Test: TestAdd
Messages: Add(2, 3) should return 5
--- FAIL: TestAdd (0.00s)
FAIL
exit status 1
FAIL example.com/mathutil 0.123s
このように、assert.Equal
を使用すると 「期待値」と「実際の値」 を明示的に比較し、テストが失敗した場合にわかりやすいメッセージが表示されます。
assert の主なメソッド
assert
パッケージには、さまざまなアサーションメソッドが用意されています。
メソッド | 説明 |
---|---|
assert.Equal(t, expected, actual) | expected と actual が等しいかチェック |
assert.NotEqual(t, expected, actual) | expected と actual が異なることを確認 |
assert.True(t, condition) | condition が true であることを確認 |
assert.False(t, condition) | condition が false であることを確認 |
assert.Nil(t, obj) | obj が nil であることを確認 |
assert.NotNil(t, obj) | obj が nil でないことを確認 |
assert.Contains(t, container, element) | container に element が含まれるか確認 |
assert.Len(t, obj, length) | obj の長さが length であるか確認 |
assert を活用した複数テスト
テーブル駆動テスト(table-driven test)でも assert を活用できます。
func TestAddTableDriven(t *testing.T) {
tests := []struct {
a, b int
expected int
}{
{1, 1, 2},
{2, 3, 5},
{-1, 1, 0},
{0, 0, 0},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("Add(%d, %d)", tt.a, tt.b), func(t *testing.T) {
result := Add(tt.a, tt.b)
assert.Equal(t, tt.expected, result, fmt.Sprintf("Add(%d, %d) should return %d", tt.a, tt.b, tt.expected))
})
}
}
assert と require の違い
同じ testify
パッケージには、assert
とは別に require
というパッケージもあります。
パッケージ | 特徴 |
---|---|
assert | エラーが発生しても次のアサーションを実行する(柔軟なテストに適している) |
require | エラーが発生した時点でテストを停止する(致命的な条件チェックに適している) |
例: require を使ったテスト
import "github.com/stretchr/testify/require"
func TestAddWithRequire(t *testing.T) {
result := Add(2, 3)
expected := 5
// 失敗するとテストを即座に中断
require.Equal(t, expected, result, "Add(2, 3) should return 5")
}
require.Equal()
を使うと、expected
と actual
が一致しなかった場合、テストは即座に中断されるため、assert
よりも厳密なテストが必要な場面に適しています。
assert を使うメリット
- 可読性の向上
- assert.Equal(t, expected, actual) のように記述でき、期待値と実際の値を明確に比較できる。
- エラー時の出力が見やすい
- Go の t.Errorf() よりも詳細な情報が表示される。
- テストの実行を継続できる
- assert はテストが失敗しても次のアサーションを実行するため、複数の条件をチェック可能。
- require を使うとテストの途中で即停止可能
- require.Equal() などを使用すると、致命的なエラーが発生した時点でテストを中断できる。
ここでは記事の最後に、以下のようなまとめセクションを追加することを提案します:
まとめ
- Go標準の
testing
パッケージによるテスト機能の提供 *_test.go
ファイルでのテストコードの作成とTestXxx
関数での実装t.Errorf
とt.Fatal
を使用したテスト失敗の報告- テーブル駆動テストによる複数入力値のテスト実装
t.Run
を使用したサブテストの作成BenchmarkXxx
関数によるベンチマークテストの実行-cover
オプションでのテストカバレッジ測定t.Helper()
によるヘルパー関数作成とエラー位置の適切な表示TestMain
関数でのテスト全体のセットアップとクリーンアップ*_test.go
以外からのテスト実行方法- サードパーティライブラリ
testify
を使用したアサーションベースのテスト実装
[Next] Step 10-1: gRPC-Gateway
[Prev] Step 8-3: Graphql go