1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. TestingYourApps
  5. Testing - Golang learning step 9-1

Testing - Golang learning step 9-1

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

roadmap.sh > Go > Testing Your Apps の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. 標準 testing パッケージ
  4. 基本的なテストの書き方
  5. t.Errorf と t.Fatal
  6. テストケースのパラメータ化
  7. サブテスト
  8. ベンチマークテスト
  9. カバレッジ測定
  10. testing.T の便利なメソッド
    1. t.Helper() を使ったヘルパー関数の作成
  11. TestMain を使ったセットアップとクリーンアップ
    1. TestMain の基本構造
    2. TestMain の具体例
    3. TestMain を使うメリット
    4. TestMain の主な用途
  12. *_test.go 以外のコードからテスト実行
    1. mathutil_test.go(通常のテスト)
    2. main.go(テストを main 関数から実行)
    3. 実行方法と出力
  13. assert() を使った Go のテスト方法
    1. testify/assert のインストール
    2. assert を使ったテストの書き方
    3. エラー発生時の出力
    4. assert の主なメソッド
    5. assert を活用した複数テスト
    6. assert と require の違い
    7. assert を使うメリット

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

標準 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.goTestMain を記述し、テストの前後でセットアップ (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)expectedactual が等しいかチェック
assert.NotEqual(t, expected, actual)expectedactual が異なることを確認
assert.True(t, condition)conditiontrue であることを確認
assert.False(t, condition)conditionfalse であることを確認
assert.Nil(t, obj)objnil であることを確認
assert.NotNil(t, obj)objnil でないことを確認
assert.Contains(t, container, element)containerelement が含まれるか確認
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() を使うと、expectedactual が一致しなかった場合、テストは即座に中断されるため、assert よりも厳密なテストが必要な場面に適しています。

assert を使うメリット

  1. 可読性の向上
    • assert.Equal(t, expected, actual) のように記述でき、期待値と実際の値を明確に比較できる。
  2. エラー時の出力が見やすい
    • Go の t.Errorf() よりも詳細な情報が表示される。
  3. テストの実行を継続できる
    • assert はテストが失敗しても次のアサーションを実行するため、複数の条件をチェック可能。
  4. require を使うとテストの途中で即停止可能
    • require.Equal() などを使用すると、致命的なエラーが発生した時点でテストを中断できる。

ここでは記事の最後に、以下のようなまとめセクションを追加することを提案します:

まとめ

  • Go標準のtestingパッケージによるテスト機能の提供
  • *_test.goファイルでのテストコードの作成とTestXxx関数での実装
  • t.Errorft.Fatalを使用したテスト失敗の報告
  • テーブル駆動テストによる複数入力値のテスト実装
  • t.Runを使用したサブテストの作成
  • BenchmarkXxx関数によるベンチマークテストの実行
  • -coverオプションでのテストカバレッジ測定
  • t.Helper()によるヘルパー関数作成とエラー位置の適切な表示
  • TestMain関数でのテスト全体のセットアップとクリーンアップ
  • *_test.go以外からのテスト実行方法
  • サードパーティライブラリtestifyを使用したアサーションベースのテスト実装


[Next] Step 10-1: gRPC-Gateway

[Prev] Step 8-3: Graphql go

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級