Go言語のテストの種類と使い分け方

はじめに

Go言語ではテストに関する機能が備わったtestingというパッケージが標準で用意されております。

今回はGo言語でテストを書く方法のうち、「テーブル駆動テスト」と「サブテスト方式」という2つの方法について見ていきます。この2つの方法を上手に使い分けることで、わかりやすくて維持しやすいテストを書くことができるのですが、使い分けについて改めて学んでみることにしました。

テーブル駆動テスト

テーブル駆動テストは、たくさんのテストパターンを1つの表(テーブル)にまとめて表現し、その表を1つずつ見ながらテストを実行する方法です。テーブル駆動テストを使うと、同じようなテストを何度も書かなくて済むので、テストコードがスッキリします。また、新しいテストパターンを追加したり、既存のテストパターンを変更したりするのも簡単になります。Go言語といえばこのテスト方式がテッパンでしょう。

以下は、テーブル駆動テストの基本的な形を示したサンプルコードです。

func TestSum(t *testing.T) {
    var tests = []struct {
        a, b int
        want int
    }{
        {1, 2, 3},
        {2, 3, 5},
        {5, 5, 10},
    }

    for _, tt := range tests {
        if got := Sum(tt.a, tt.b); got != tt.want {
            t.Errorf("Sum(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

このサンプルコードでは、testsという表(スライス)にテストパターンを定義しています。各テストパターンは、入力する値ab、期待する結果wantを持つ構造体で表現されています。

forループを使ってテストパターンを1つずつ取り出し、Sum関数を呼び出します。関数の結果と期待する結果を比べて、違う場合はエラーを報告します。

テーブル駆動テストは、以下のようなときに特に効果的です。

  • 同じ関数に対して、いろいろな入力値と期待する結果のパターンがあるとき。
  • 関数の動きが入力値によって大きく変わらないとき。
  • テストパターンの数が多いとき。

テーブル駆動テストを使うことで、テストコードが読みやすくなり、管理しやすくなります。また、テストパターンを追加したり変更したりするのが簡単になるので、テストを長く使い続けることができます。

サブテスト方式

サブテスト方式は、1つのテスト関数の中で、さらに細かいテストを実行する方法です。t.Runという機能を使って、テスト関数の中にサブテストを作ります。サブテストを使うと、テストの目的や条件ごとにテストを分けて書くことができるので、テストコードが読みやすくなります。

以下は、サブテスト方式を使ったサンプルコードです。

func TestSplit(t *testing.T) {
    t.Run("Normal", func(t *testing.T) {
        got := Split("a,b,c", ",")
        want := []string{"a", "b", "c"}
        if !reflect.DeepEqual(got, want) {
            t.Errorf("Split(\"a,b,c\", \",\") = %v, want %v", got, want)
        }
    })

    t.Run("Empty", func(t *testing.T) {
        got := Split("", ",")
        want := []string{}
        if !reflect.DeepEqual(got, want) {
            t.Errorf("Split(\"\", \",\") = %v, want %v", got, want)
        }
    })

    t.Run("No delimiter", func(t *testing.T) {
        got := Split("abc", "")
        want := []string{"a", "b", "c"}
        if !reflect.DeepEqual(got, want) {
            t.Errorf("Split(\"abc\", \"\") = %v, want %v", got, want)
        }
    })
}

このサンプルコードでは、TestSplitという1つのテスト関数の中に、3つのサブテスト("Normal""Empty""No delimiter")を作っています。それぞれのサブテストでは、Split関数に対して異なる入力値を与え、期待する結果と比較しています。

サブテスト方式は、以下のようなときに特に効果的です。

  • テストしたい関数の使い方が複雑で、いくつかの異なる条件下でテストする必要があるとき。
  • テストパターンごとに、テストの準備(セットアップ)や後片付け(クリーンアップ)が異なるとき。
  • テストコードを読みやすくしたいとき。

サブテスト方式を使うことで、テストの目的や条件が明確になり、テストコードの理解が深まります。また、特定の条件だけをテストしたいときにも、サブテストを個別に実行できるので便利です。

テーブル駆動テストとサブテスト方式の使い分け

テーブル駆動テストとサブテスト方式は、それぞれ適した使い方があります。以下のようなポイントを考慮して、2つの方式を使い分けましょう。

  1. テストパターンの似ている度合い

    • テストパターンの内容が似ていて、同じような手順でテストできる場合は、テーブル駆動テストが適しています。
    • テストパターンごとに、異なる手順や条件が必要な場合は、サブテスト方式の方が良いでしょう。
  2. テストの準備と後片付けの共通性

    • テストパターンごとに、テストの準備や後片付けが異なる場合は、サブテスト方式を使うことで、それぞれのテストパターンに合わせた準備と後片付けができます。
    • テストの準備と後片付けが共通の場合は、テーブル駆動テストを使うとシンプルに書けます。
  3. テストコードの読みやすさ

    • テーブル駆動テストは、入力値と期待する結果を表形式で表現するので、テストパターンが一目で分かりやすくなります。
    • サブテスト方式は、それぞれのテストパターンに名前を付けられるので、テストの目的や内容が明確になります。
  4. テストパターンの数

    • テストパターンの数が多い場合は、テーブル駆動テストを使うことで、コードの繰り返しを減らし、テストを管理しやすくなります。
    • テストパターンの数が少ない場合は、サブテスト方式でも十分に管理できます。

表にしてみました↓

観点 テーブル駆動テスト サブテスト方式
テストパターンの類似性 高い 低い
テストの準備と後片付け 共通 個別
テストコードの読みやすさ テストパターンが一目で分かる テストの目的や内容が明確
適したテストパターンの数 多い 少ない

まとめ

使い分け方が大体整理できました。これを活かすのであれば、ハンドラーなどの中心的な処理はテストしたいパターンが多くなることがあるのでサブテストにして、そのハンドラー内で呼び出しているDBアクセスのメソッドはテーブル駆動テストにするのが良さそうです。

参考資料

pkg.go.dev

go.dev