Table driven testについて調べた

sql-maskのPRのレビューgotestsというものを紹介していただき,Table driven testが気になったので調べました.今回は調べた内容をTable driven testでのテストコードの書き方を例とともに書き留めておきたいと思います. また,Table driven testでsqdsql-maskのテストコードをリファクタリングしたので,感想をメモしておきたいと思います.

Table driven testについて

Table driven testはGoのテストプラクティスの一つで,テーブル(配列)に入力値と期待する結果のセット(テストケース)を定義します.このテーブルにはテスト名などの追加情報が含まれることもあります.このテストケースを記述したテーブルをループ処理によって各項目ごとにテストを行います.Golangでは,gotestsを利用することで,Table driven testに基づいたテーブルとテスト処理の雛形を作成することができます.

Table driven testでのテストコードの書き方

以下のようなfizzbuzzの処理を行うメソッドに対してgotestsを使ってテストを書いていきたいと思います.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    for i := 1; i <= 50; i++ {
        fmt.Println(fizzbuzz(i))
    }
}

func fizzbuzz(i int) string {
    if i%15 == 0 {
        return "FizzBuzz"
    } else if i%3 == 0 {
        return "Fizz"
    } else if i%5 == 0 {
        return "Buzz"
    } else {
        return strconv.Itoa(i)
    }
}

このコードの fizzbuzz()に対して, gotestsを適用すると以下の雛形が出力されます.

package main

import "testing"

func Test_fizzbuzz(t *testing.T) {
    type args struct {
        i int
    }
    tests := []struct {
        name string
        args args
        want string
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := fizzbuzz(tt.args.i); got != tt.want {
                t.Errorf("fizzbuzz() = %v, want %v", got, tt.want)
            }
        })
    }
}

この雛形に従ってテストケースを追記していきます.

func Test_fizzbuzz(t *testing.T) {
    type args struct {
        i int
    }
    tests := []struct {
        name string
        args args
        want string
    }{
        {
            name: "Return number string",
            args: args{i: 1},
            want: "1",
        },
        {
            name: `Return "Fizz"`,
            args: args{i: 3},
            want: "Fizz",
        },
        {
            name: `Return "Buzz"`,
            args: args{i: 5},
            want: "Buzz",
        },
        {
            name: `Return "FizzBuzz"`,
            args: args{i: 15},
            want: "FizzBuzz",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := fizzbuzz(tt.args.i); got != tt.want {
                t.Errorf("fizzbuzz() = %v, want %v", got, tt.want)
            }
        })
    }
}

Table driven testを書いてみて思ったこと

このTable driven testを使って,sqdとsql-maskのテストコードをリファクタリングしました. その過程でTable driven testを利用することで以下のような利点があると思いました.

  • テストケースの可読性が向上し管理しやすくなる
  • 何のテスト(name)をするために,何を入力(args)して,どういう出力(want)が欲しいのかが明確になる
  • テーブルをループしてテストするので,テストの処理やエラー処理が共通化される

また,Table driven testが書きにくい場合は以下のようなことに原因があると感じました.

  1. 入出力が多くて複雑
  2. 出力に影響を与える要素が入力以外に含まれている

1.はメソッドの切り分けが適切にできていない可能性があると思いました.2.は依存関係が多くなっていることからコードが複雑化しており,必要な依存関係かを確認する必要があると思いました.このようにTable driven testを使うことで,これらの問題を発見でき解消するきっかけになると思いました.

参考