Goにproperty based testingを布教したい

4 min読了の目安(約3800字TECH技術記事

property based testingという超絶便利なテスト手法を布教していく。

そもそもproperty based testingとは

  • 半自動で値を生成し、それに対してテストを行う手法
  • HaskellのQuickCheckが元祖?(多分)
  • 以下property based testingをpbtと呼ぶ
  • HaskellとかScalaとか関数型でめっちゃ使う

普通にテストするときの課題

  • 普通にテストするときは関数なりになにか値を渡してテストする
  • しかしこれはあくまでもその値に対してにしか正当性が保証できない
  • エッジケースや例外ケースは人間が考えてテストケースを用意する必要がある
  • サンプリングに近い

pbtでは

  • 値は半自動で生成され、膨大な数のテストケースに対してテストが実行される
  • 人間が考えるのはどの範囲の値に対してテストをするかということであり、その実際の値について(考えてもいいが)考える必要が少なくなる
  • またテストケースが自動生成されるためテストのコストも軽くなる
  • 最小失敗ケース(後述)を特定する仕組みも備わっており、実装ミスを特定しやすくなっている
  • テストと言うよりも証明にちかい

pbtの道具箱

Generator

  • 値を生成する役割を持つもの
  • ある範囲、あるオブジェクトを生成する役割を担う
    • 任意の整数や文字列
    • nameとして任意の文字列を持つDogクラス
  • Generatorは組み合わせる事によって複雑なGeneratorを作成することができる
    • プリミティブ型のgeneratorからその値を持つクラスを生成する
  • libraryによって名前はまちまち

Property

  • テストケースに相当するもの
  • 具体的に記述する内容はGeneratorが生成する値に対して、関数を呼ぶと出力として〜が返るといったテスト内容を記述する
  • ここでGeneratorを使用したテストを記述することで膨大な数の入力に対してテストが実行される

Shrink

  • テストの最小失敗ケースを自動特定するための仕組み
  • Generatorが担保する範囲の値には小ささのようなものが存在する
    • 例えば整数の範囲では0が最も小さい
    • 文字列では空文字列
  • テストが失敗したときに、そのテストで入力した値からshrink(縮ませる)して値を小さくしていき、テストが失敗する値の中で最も小さいものを探す
    • 10が入力のときに失敗したら5で試してみる
    • 5でも失敗したら次は2
    • 2で成功したら5が最小失敗ケース
  • 実際のshrinkはテストフレームワークが勝手にやってくれるのでユーザは気にする必要はほぼない
    • ただしshrinkのロジックを変更したいときなどは適宜実装が必要

各言語でのpbtの実装例

gopter

  • goでpbtを行えるライブラリ

  • 例は公式から
func TestSqrt(t *testing.T) {
	properties := gopter.NewProperties(nil)

	properties.Property("greater one of all greater one", prop.ForAll(
		func(v float64) bool {
			return math.Sqrt(v) >= 1
		},
		gen.Float64Range(1, math.MaxFloat64),
	))

	properties.Property("squared is equal to value", prop.ForAll(
		func(v float64) bool {
			r := math.Sqrt(v)
			return math.Abs(r*r-v) < 1e-10*v
		},
		gen.Float64Range(0, math.MaxFloat64),
	))

	properties.TestingRun(t)
}

例の解説

  • テスト対象は入力値のルートを取る関数math.Sqrt
  • テスト項目は2つ
    1. 1以上の入力に対しては出力も1以上になる
    2. 0以上の入力に対しての出力を二乗したものは入力に等しい(例では近似値比較)
  • これらのテスト項目が指定された範囲の様々な値に対して自動でテストされる
  • これらのproperty=性質を満たすような関数math.Sqrtはまさにルートとして振る舞うと言っていい

Generatorの作成

  • goではGeneratorのことをgenという
  • stringなどのgo組み込みの値を生成するgenはgopterにもともと備わっている
  • しかしpbtの強力な点は自分の好きな値を生成するgenを作成できること
    • これがpbtの大きな強みの一つと言っていい
    • 生成したい値が変わった場合はgenも変えればそれに合わせてすべてのテストケースも修正される

structのgen

  • ランダム値をもつstructが勝手に生成されます
type Dog struct {
    Name string
    Age int32
}

dogGen := gen.Struct(reflect.TypeOf(&Dog{}), map[string]gopter.Gen {
    "Name": gen.AlphaString(),
    "Age": gen.Int32(),
})

あるgenの値を使用して別の値を生成するgenの作成

  • Map使いましょう
// upper caseのアルファベットを生成するgen
upperAlphaStringGen := gen.AlphaString().Map(strings.ToUpper)

条件を満たす値のみを生成するgen

  • SuchThat使いましょう
// 文字数が2のアルファベットのみ生成するgen
len2AlphaStringGen := gen.AlphaString().SuchThat(func(s string) bool {
	return utf8.RuneCountInString(s) == 2
})

genにラベルをつける

  • WithLabelでgenに名前をつけるとテスト結果の表示がわかりやすくなるのでおすすめです

他にも便利なオペレータが色々あるんで調べたら幸せになります。

Testparamters

  • pbtをする際に必要なテストの設定を行うことができる
  • 指定しなければデフォルト値
  • カスタマイズする際には1から自分で作っていいがgopter.DefaultTestParametersというメソッドでparamを生成してそれを差分変更するのがいい
param := gopter.DefaultTestParameters()
param.MinSize = 3
param.MinSuccessfulTests = 10
props := gopter.NewProperties(param)

MinSuccessfulTests

  • Propertyは一度走らせるとデフォルトでは100個の値に対して成功するかを検証してくれる
  • テストが重いときは100回走らせたくない
  • 最低成功回数を設定するためにこのパラメタを指定する
// テストをpassするのに必要な成功回数が3回になる
params := gopter.DefaultTestParameters()
params.MinSuccessfulTests = 3
props := gopter.NewProperties(params)

MinSize

  • Genで生み出されるSliceの最小の長さを設定する
  • gen.SliceOfはデフォルトでは任意のランダムな長さのsliceを生成するがその最小長さが指定される

最後に

みんなもpbtしてバグを減らし幸せになろう!

この記事に贈られたバッジ