Go言語入門:よくある質問 -Packages and Testing Vol.2-

スポンサーリンク
Go言語入門:よくある質問 -Packages and Testing Vol.2- ノウハウ
Go言語入門:よくある質問 -Packages and Testing Vol.2-
この記事は約14分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のよくある質問 について解説しています。

スポンサーリンク

背景

Go言語を学んでいると「なんでこんな仕様になっているんだろう?」「他の言語と違うのはなぜ?」といった疑問が湧いてきませんか。Go言語の公式サイトにあるFAQページには、そんな疑問に対する開発チームからの丁寧な回答がたくさん載っているんです。ただ、英語で書かれているため読むのに少しハードルがあるのも事実で、今回はこのFAQを日本語に翻訳して、Go言語への理解を深めていけたらと思い、これを読んだ時の内容を備忘として残しました。

Packages and Testing

ユニットテストを書くにはどうすればよいですか?

パッケージのソースと同じディレクトリに、_test.goで終わる新しいファイルを作成してください。そのファイル内で、import "testing"し、次の形式の関数を書いてください:

func TestFoo(t *testing.T) {
    ...
}

そのディレクトリでgo testを実行してください。このスクリプトはTest関数を見つけ、テストバイナリをビルドし、実行します。

詳細については、How to Write Go Codeドキュメント、testingパッケージ、およびgo testサブコマンドを参照してください。

解説

ユニットテストとは?

ユニットテストは、プログラムの各部分(関数やメソッド)が正しく動作するかを自動的にチェックするテストです。バグを早期に発見し、コードの品質を保つために重要です。

基本的な用語

  • テストファイル: _test.goで終わるファイル
  • テスト関数: Testで始まる関数
  • testing.T: テスト結果を報告するためのオブジェクト
  • アサーション: 期待する結果と実際の結果を比較すること

ステップバイステップ: 最初のテストを書く

ステップ1: テスト対象のコードを書く

math.go:

package math

// Add は2つの整数を足し算します
func Add(a, b int) int {
    return a + b
}

// Multiply は2つの整数を掛け算します
func Multiply(a, b int) int {
    return a * b
}
ステップ2: テストファイルを作る

ファイル名のルール: 元のファイル名 + _test.go

mypackage/
  ├── math.go       ← 元のファイル
  └── math_test.go  ← テストファイル
ステップ3: テストを書く

math_test.go:

package math

import "testing"

// Test で始まる関数名
func TestAdd(t *testing.T) {
    // テストケース: 2 + 3 = 5 になるはず
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

func TestMultiply(t *testing.T) {
    // テストケース: 4 * 5 = 20 になるはず
    result := Multiply(4, 5)
    expected := 20
    
    if result != expected {
        t.Errorf("Multiply(4, 5) = %d; want %d", result, expected)
    }
}
ステップ4: テストを実行する
# ディレクトリに移動
cd mypackage

# テストを実行
go test

成功した場合の出力:

PASS
ok      mypackage    0.002s

失敗した場合の出力:

--- FAIL: TestAdd (0.00s)
    math_test.go:10: Add(2, 3) = 6; want 5
FAIL
exit status 1
FAIL    mypackage    0.003s

テスト関数の書き方

基本的な構造
func TestXxx(t *testing.T) {
    // 1. 準備 (Arrange)
    input := 10
    
    // 2. 実行 (Act)
    result := MyFunction(input)
    
    // 3. 検証 (Assert)
    if result != expected {
        t.Errorf("got %v, want %v", result, expected)
    }
}
テスト関数の命名規則

正しい命名:

func TestAdd(t *testing.T)           // OK
func TestUserLogin(t *testing.T)     // OK
func TestCalculateTax(t *testing.T)  // OK

間違った命名:

func testAdd(t *testing.T)           // NG: 小文字で始まっている
func Testadd(t *testing.T)           // NG: addが小文字
func AddTest(t *testing.T)           // NG: Testで始まっていない

testing.T のメソッド

よく使うメソッド
func TestExample(t *testing.T) {
    // エラーを報告するが、テストを続行
    t.Error("何か問題が発生しました")
    t.Errorf("期待値: %d, 実際: %d", 10, 20)
    
    // エラーを報告し、テストを即座に停止
    t.Fatal("致命的なエラー")
    t.Fatalf("致命的なエラー: %v", err)
    
    // ログを出力(verbose モードで表示)
    t.Log("デバッグ情報")
    t.Logf("値: %v", value)
    
    // テストをスキップ
    t.Skip("このテストはスキップします")
    t.Skipf("条件 %v のためスキップ", condition)
}
Error vs Fatal の違い
func TestComparison(t *testing.T) {
    // Error: エラーを報告するが、後続のテストを実行
    t.Error("test 1 failed")
    t.Error("test 2 failed")  // これも実行される
    
    // Fatal: エラーを報告し、即座に終了
    t.Fatal("test 3 failed")
    t.Error("test 4 failed")  // これは実行されない!
}

テーブルドリブンテスト(推奨パターン)

複数のテストケースを効率的に書く方法:

func TestAdd(t *testing.T) {
    // テストケースをテーブルで定義
    tests := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"正の数", 2, 3, 5},
        {"負の数", -1, -1, -2},
        {"ゼロ", 0, 5, 5},
        {"大きな数", 1000, 2000, 3000},
    }
    
    // 各テストケースを実行
    for _, tt := range tests {
        t.Run(tt.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)
            }
        })
    }
}

実行結果:

go test -v
=== RUN   TestAdd
=== 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: TestAdd/大きな数 (0.00s)
PASS

便利なgoコマンドオプション

# 詳細な出力(-v: verbose)
go test -v

# 特定のテスト関数だけ実行
go test -run TestAdd

# カバレッジを表示
go test -cover

# カバレッジの詳細をHTMLで表示
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

# すべてのサブパッケージをテスト
go test ./...

# 並列実行の数を指定
go test -parallel 4

実践的な例: 構造体のテスト

user.go:

package user

type User struct {
    Name  string
    Email string
    Age   int
}

func (u *User) IsAdult() bool {
    return u.Age >= 18
}

func (u *User) IsValidEmail() bool {
    return strings.Contains(u.Email, "@")
}

user_test.go:

package user

import "testing"

func TestUser_IsAdult(t *testing.T) {
    tests := []struct {
        name     string
        age      int
        expected bool
    }{
        {"未成年", 15, false},
        {"ちょうど18歳", 18, true},
        {"成人", 25, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            u := &User{Age: tt.age}
            result := u.IsAdult()
            if result != tt.expected {
                t.Errorf("Age %d: got %v, want %v", 
                    tt.age, result, tt.expected)
            }
        })
    }
}

func TestUser_IsValidEmail(t *testing.T) {
    tests := []struct {
        name     string
        email    string
        expected bool
    }{
        {"有効なメール", "test@example.com", true},
        {"無効なメール", "invalid-email", false},
        {"空のメール", "", false},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            u := &User{Email: tt.email}
            result := u.IsValidEmail()
            if result != tt.expected {
                t.Errorf("Email %q: got %v, want %v", 
                    tt.email, result, tt.expected)
            }
        })
    }
}

エラーハンドリングのテスト

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func TestDivide(t *testing.T) {
    // 正常ケース
    t.Run("正常な割り算", func(t *testing.T) {
        result, err := Divide(10, 2)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if result != 5 {
            t.Errorf("got %d, want 5", result)
        }
    })
    
    // エラーケース
    t.Run("ゼロ除算", func(t *testing.T) {
        _, err := Divide(10, 0)
        if err == nil {
            t.Fatal("expected error, got nil")
        }
    })
}

ベストプラクティス

1. テストは独立させる
// ❌ 悪い例: テスト間で状態を共有
var counter int

func TestA(t *testing.T) {
    counter++  // 他のテストに影響
}

// ✅ 良い例: 各テストで独立した状態
func TestB(t *testing.T) {
    counter := 0  // ローカル変数
    counter++
}
2. 分かりやすいエラーメッセージ
// ❌ 悪い例
if result != expected {
    t.Error("failed")
}

// ✅ 良い例
if result != expected {
    t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, expected)
}
3. テーブルドリブンテストを使う
// ✅ 推奨: 複数のケースをまとめて管理
tests := []struct{
    name string
    input int
    want int
}{
    {"ケース1", 1, 2},
    {"ケース2", 2, 4},
}

まとめ

Goでユニットテストを書くのはとてもシンプル:

  1. _test.goで終わるファイルを作る
  2. import "testing"する
  3. func TestXxx(t *testing.T)の形式で書く
  4. go testで実行
  5. ✅ テーブルドリブンテストで複数ケースを効率的に

Goのテストの特徴

  • シンプル: 外部ライブラリ不要、標準機能だけでOK
  • 高速: 並列実行が簡単
  • 統一: すべてのGoプロジェクトで同じ方法
  • 組み込み: go testコマンド1つで実行

テストを書くことで:

  • 🐛 バグを早期発見
  • 📝 コードの仕様を文書化
  • 🔧 リファクタリングが安全に
  • 💪 コードの品質が向上

最初は面倒に感じるかもしれませんが、テストを書く習慣をつけることで、長期的には開発がずっと楽になります!

おわりに 

本日は、Go言語のよくある質問について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました