emahiro/b.log

Drastically Repeat Yourself !!!!

Table Driven Testのテストケースの書き方について

Goでは単体テストを書く場合に Table Driven Test が可読性、保守性の観点から推奨されています。

そのTableDrivenTestにおいてテストケースの書き方について備忘録です。

テストケースの書き方

ケースごとのstructを定義する

以下のようなコードを想定します。

type sampleUseCase struct{
  field1 int
  field2 string
}

func TestHoge(t *testing.T){
  cases := []sampleUseCases {
    {
      field1:1,
      filed2:"hoge",
    },
    // 各テストケース
  }
  
  for _ , tt := range cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • テストのユースケースがわかりやすい
  • 一方でテストケースをいちいち外部に出さなくてもよく、Goぽくない。
  • たまに公開structにしているケースもあるが、テストケースを公開structとして外部package向けにも公開するべきではない。
    • 影響範囲を最小にするため

Testメソッドの中に非公開structを定義する

こちらが一般的な方法。コードは以下。

func TestHoge(t *testing.T){
  
  cases := []strict{
    field1 int
    field2 string
  }{
    {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • GoらしいTableDrivenTest
  • テストメソッド内で非公開structにしているので、外部への影響がない。

structのslice or map[string]struct

これは好みだと思いますが、ユニットテストを書くケースにおいてstructのsliceにするか、mapでテストケースを書くか2パターンがあると思います。
Goのテストではテストメソッドごとsubtestにしてテストするのがいいとされています。

理由は

  • subtestの第一引数に「なんのテストを実行するのか」名前をつけられること
  • deferが書きやすいこと
  • リクエストなどを含まない単純なパターンのテストの場合、並列実行により高速が直列実行するときよりもテストのパフォーマンスを向上されらるため。

この1つ目のテストケースに名前をつけられる、というところですが具体的なコードだと以下のようになります。

func TestHoge(t *testing.T){
  
  cases := []strict{
    name   string
    field1 int
    field2 string
  }{
    {
      name:   "test case 1"
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(tt.name, func(t *testing.T){
      // テストを書く
    })
  }
}

これでも十分わかりやすいですが、テストの名前がわかるためだけに name filedを増やすのも微妙だと思ってました。 そこで考えたのが、nameをkeyにした map[string]struct を作成してテストケースにするプランです。

func TestHoge(t *testing.T){
  
  cases := map[string]strict{
    field1 int
    field2 string
  }{
    "field1 case": {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for k, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(k, func(t *testing.T){
      // mapのkeyがそのままテストケースになる。
      // テストを書く
    })
  }
}

このようにテストケースのstructの定義の仕方はいくつかありますが、mapが使いやすいので今後はmapでテストケースを書いていこうと思います。