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

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

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

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

スポンサーリンク

背景

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

Packages and Testing

テスト用のお気に入りのヘルパー関数はどこにありますか?

Goの標準testingパッケージはユニットテストを書くのを簡単にしますが、他の言語のテストフレームワークで提供されているアサーション関数などの機能が欠けています。このドキュメントの前のセクションで、なぜGoにアサーションがないのかを説明しましたが、同じ議論がテストでのassertの使用にも当てはまります。適切なエラー処理とは、1つのテストが失敗した後も他のテストを実行させることを意味します。そうすることで、失敗をデバッグする人が何が間違っているかの全体像を把握できます。テストが「isPrimeは2に対して間違った答えを返し、そのためこれ以上テストは実行されませんでした」と報告するよりも、「isPrimeは2、3、5、7に対して間違った答えを返します(または2、4、8、16に対して)」と報告する方がはるかに有用です。テストの失敗をトリガーするプログラマーは、失敗するコードに精通していない可能性があります。良いエラーメッセージを書くために今投資した時間は、テストが壊れたときに後で報われます。

関連する点として、テストフレームワークは条件分岐や制御、印刷メカニズムを備えた独自のミニ言語に発展する傾向がありますが、Goにはすでにそれらすべての機能があります。なぜそれらを再作成する必要があるのでしょうか? 私たちはテストをGoで書きたいのです。学ぶべき言語が1つ少なくなり、このアプローチはテストを素直で理解しやすく保ちます。

良いエラーを書くために必要な追加のコードの量が反復的で圧倒的に見える場合、テストはテーブルドリブンにし、データ構造で定義された入力と出力のリストを反復処理する方がうまく機能する可能性があります(Goはデータ構造リテラルに優れたサポートを持っています)。良いテストと良いエラーメッセージを書く作業は、多くのテストケースに償却されます。標準Goライブラリは、fmtパッケージのフォーマットテストなど、実例に満ちています。

解説

この問題は何について説明しているの?

他のプログラミング言語ではアサーション関数(assert関数)という便利な機能がありますが、Goには標準で含まれていません。「なぜないの? どうすればいいの?」という疑問に答える内容です。

基本的な用語

  • アサーション関数: 期待する結果と実際の結果を比較し、違っていたら自動的にテストを失敗させる関数
  • ヘルパー関数: テストコードを簡潔に書くための補助関数
  • テストフレームワーク: テストを書きやすくするためのライブラリやツール

他の言語のアサーション関数とは?

例: 他の言語(疑似コード)
// JavaScriptのJestなど
test('足し算のテスト', () => {
    expect(add(2, 3)).toBe(5);           // 短くてシンプル!
    expect(multiply(4, 5)).toBe(20);     // 短くてシンプル!
});
# PythonのunittestやPytestなど
def test_add():
    assert add(2, 3) == 5                # 1行で書ける!
    assert multiply(4, 5) == 20          # 1行で書ける!

特徴: 簡潔で書きやすい!

Goの場合

標準的な書き方
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
    
    result = Add(10, 20)
    if result != 30 {
        t.Errorf("Add(10, 20) = %d; want 30", result)
    }
}

感想: 「冗長じゃない? もっと短く書きたい!」

なぜGoにはアサーション関数がないのか?

理由1: 全体像を把握するため

アサーション関数がある場合:

// もしアサーション関数があったら...
func TestIsPrime(t *testing.T) {
    assert.Equal(t, isPrime(2), true)   // ここで失敗
    assert.Equal(t, isPrime(3), true)   // 実行されない
    assert.Equal(t, isPrime(5), true)   // 実行されない
    assert.Equal(t, isPrime(7), true)   // 実行されない
}

問題点: 最初のテストで失敗すると、残りのテストが実行されない

  • → 2だけ失敗していることしか分からない
  • → 「3, 5, 7は動くのか? それも壊れてるのか?」が不明

Goの場合:

func TestIsPrime(t *testing.T) {
    if !isPrime(2) {
        t.Errorf("isPrime(2) = false; want true")
    }
    if !isPrime(3) {
        t.Errorf("isPrime(3) = false; want true")
    }
    if !isPrime(5) {
        t.Errorf("isPrime(5) = false; want true")
    }
    if !isPrime(7) {
        t.Errorf("isPrime(7) = false; want true")
    }
}

利点: すべてのテストが実行される

  • → 「2, 3, 5, 7すべてで失敗している」と分かる
  • → 問題の全体像が見える!
理由2: 良いエラーメッセージが書ける

アサーション関数の場合:

Expected: 5
Actual: 6

→ 「何の計算で失敗したか分からない…」

Goの場合:

t.Errorf("Add(2, 3) = %d; want 5", result)

→ 出力: Add(2, 3) = 6; want 5 → 「Add関数に2と3を渡したら6が返ってきた。5が正しいのに」と明確!

理由3: Goという言語をそのまま使える

他のテストフレームワークはミニ言語になりがち:

// 複雑なテストフレームワークの例
describe('Calculator', () => {
    context('when adding numbers', () => {
        it('should return correct sum', () => {
            expect(add(2, 3)).to.equal(5);
        });
    });
});

新しいキーワード: describe, context, it, expect, to.equal… → 覚えることが増える!

Goの場合:

func TestCalculator_Add(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("got %d, want 5", result)
    }
}

→ 普通のGo言語だけ! 新しい言語を学ぶ必要なし!

「でも、冗長すぎない?」への答え

解決策: テーブルドリブンテスト

繰り返しを減らすGoらしい方法:

❌ 冗長な書き方:

func TestIsPrime(t *testing.T) {
    if !isPrime(2) {
        t.Errorf("isPrime(2) = false; want true")
    }
    if !isPrime(3) {
        t.Errorf("isPrime(3) = false; want true")
    }
    if !isPrime(5) {
        t.Errorf("isPrime(5) = false; want true")
    }
    if !isPrime(7) {
        t.Errorf("isPrime(7) = false; want true")
    }
    if isPrime(4) {
        t.Errorf("isPrime(4) = true; want false")
    }
    // ...繰り返し...
}

✅ Goらしい書き方(テーブルドリブン):

func TestIsPrime(t *testing.T) {
    tests := []struct {
        name     string
        input    int
        expected bool
    }{
        {"素数2", 2, true},
        {"素数3", 3, true},
        {"素数5", 5, true},
        {"素数7", 7, true},
        {"非素数4", 4, false},
        {"非素数6", 6, false},
        {"非素数8", 8, false},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := isPrime(tt.input)
            if result != tt.expected {
                t.Errorf("isPrime(%d) = %v; want %v", 
                    tt.input, result, tt.expected)
            }
        })
    }
}

利点:

  • ✅ テストケースの追加が簡単(1行追加するだけ)
  • ✅ エラーメッセージは詳細
  • ✅ すべてのテストが実行される
  • ✅ コードが整理されている

実践例: 良いエラーメッセージの価値

悪いエラーメッセージ
func TestCalculation(t *testing.T) {
    if calculateTax(1000) != 100 {
        t.Error("failed")  // これだけ
    }
}

出力:

--- FAIL: TestCalculation (0.00s)
    test.go:10: failed

→ 「何が失敗したの? 実際の値は? 期待値は?」全部不明!

良いエラーメッセージ
func TestCalculation(t *testing.T) {
    input := 1000
    expected := 100
    result := calculateTax(input)
    
    if result != expected {
        t.Errorf("calculateTax(%d) = %d; want %d (税率10%%想定)", 
            input, result, expected)
    }
}

出力:

--- FAIL: TestCalculation (0.00s)
    test.go:15: calculateTax(1000) = 150; want 100 (税率10%想定)

→ 「あ、税率が15%で計算されてる! 10%であるべきなのに」とすぐ分かる!

標準ライブラリの実例

Goの標準ライブラリ(fmtパッケージのテストなど)も同じパターンを使っています:

// fmtパッケージのテストの例(簡略版)
var fmtTests = []struct {
    fmt string
    val any
    out string
}{
    {"%d", 12345, "12345"},
    {"%v", 12345, "12345"},
    {"%t", true, "true"},
}

func TestSprintf(t *testing.T) {
    for _, tt := range fmtTests {
        out := fmt.Sprintf(tt.fmt, tt.val)
        if out != tt.out {
            t.Errorf("Sprintf(%q, %v) = %q; want %q", 
                tt.fmt, tt.val, out, tt.out)
        }
    }
}

もしどうしてもアサーション関数が欲しい場合

サードパーティライブラリもあります(ただし推奨はされない):

import "github.com/stretchr/testify/assert"

func TestAdd(t *testing.T) {
    assert.Equal(t, 5, Add(2, 3))  // アサーション関数
}

ただし:

  • Goコミュニティでは標準的な方法が推奨される
  • 標準ライブラリだけで十分な場合が多い
  • テーブルドリブンテストの方がGoらしい

まとめ

Goにアサーション関数がない理由
  1. 全体像の把握: 1つ失敗しても残りを実行し、全問題を把握
  2. 詳細なエラー: 何が、どう間違っているか明確に
  3. シンプルさ: 新しいミニ言語ではなく、普通のGoで書く
冗長性の解決
  • 📊 テーブルドリブンテストを使う
  • 📝 良いエラーメッセージを書く(後で報われる)
  • 🔄 繰り返しを構造化する
Goの哲学

Goは「明示的で、シンプルで、予測可能」であることを重視します:

  • マジックな機能より、明確なコード
  • 短いコードより、分かりやすいコード
  • 便利さより、保守性

最初は冗長に感じるかもしれませんが:

  • デバッグ時に詳細な情報が得られる
  • 数ヶ月後に見てもすぐ理解できる
  • チーム全員が同じパターンを使える

テーブルドリブンテストをマスターすれば、冗長さは気にならなくなり、むしろGoのテストの書きやすさに気づくはずです!

おわりに 

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

よっしー
よっしー

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

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

コメント

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