Property Based Testing とは
Property Based Testing は入力値を大量にかつランダムに生成し、テストコードが性質を満たしているかどうかを検証するテスト手法です。
- ScalaCheck + ScalaTest で Property-Based Testing する(classmethod.jp)
- Property Based Testingでドメインロジックをテストする(gakuzzzzさん)
testing/quick はやや物足りない
Goの標準ライブラリにも似たような機能として testing/quick
が用意されています。 Gopher Academy Blog に詳しく説明が書かれていました 。
ランダムな値を生成するといっても、実際には「0以上のintが欲しい」のような、一定の条件を満たした値が欲しいはずです。testing/quick
はお手軽に使えるという点では良いのですが、実装は非常にシンプルになっており、複雑な条件を指定することができずかゆいところに手が届きません。また、現在はコードがフリーズされているようで、これ以上の機能拡張は今後もされる予定はなさそうです。
gopter とは
gopter (https://github.com/leanovate/gopter) は Go言語で書かれた Property Based Testing のためのライブラリです。 HaskellのQuickCheck, ScalaのScalaCheck を参考にして作られていて、それらを利用したことがある人には取っつきやすいI/Fになっています。
以下のような機能で構成されています
- 乱数を生成するgen
- genを実際の型情報と照らし合わせていい感じに使えるようにする(?)arbitrary
- どのようにテストをするかのコンテキストを持つprop
- ステートフルなテストを実行するためのヘルパーを提供するcommands
基本的な使い方
使い方を簡単に紹介します。
プリミティブなランダム値を生成する
gen
パッケージに数値や文字列などを自動生成するための機能が揃っています。ジェネレーターはSample()
で値を生成します。ジェネレータを用意するときはgenパッケージの中に自分の欲しいものがすでに存在するか確かめると良いです。
// intを生成するgenerator
numberGenerator := gen.Int()
// stringを生成するgenerator
strGenerator := gen.AnyString()
// アルファベット文字列を生成するgenerator
alphabetGenerator := gen.AlphaString()
// true or falseを生成するgenerator
boolGenerator := gen.Bool()
// 数値のポインタを生成する
numPtrGenerator := gen.PtrOf(numberGenerator)
for i := 1; i < 10; i++ {
num, _ := numberGenerator.Sample()
str1, _ := strGenerator.Sample()
str2, _ := alphabetGenerator.Sample()
str3, _ := boolGenerator.Sample()
// Sample() は interface{} を返すため型は自分で与えてやらなければなりません
fmt.Println(num.(int), str1.(string), str2.(string), str3.(string))
}
ジェネレーターを組み合わせる
generatorは組み合わせて新たなgeneratorを作ることができます。
// ポインタを生成するジェネレーター
numPtrGenerator := gen.PtrOf(numberGenerator)
strPtrGenerator := gen.PtrOf(strGenerator)
// スライスを生成するジェネレーター
numSliceGenerator := gen.SliceOf(numberGenerator)
strSliceGenerator := gen.SliceOfN(10, strGenerator)
// 複数のジェネレーターをまとめる
// Int32とFloat32が入ったinterface{} sliceを生成する ex: []interface{}{1487979884 -1.18763985e-07}
twoValGen := gopter.CombineGens(gen.Int32(), gen.Float32())
structのランダム値を生成する
structを作る際は、フィールド名とジェネレーターのmapを情報として渡します。
type User struct {
Name string
Address []string
// age int64 privateなフィールドのジェネレーター指定はできない。後ほど解決方法を書きます
}
userGen := gen.Struct(reflect.TypeOf(&User{}), map[string]gopter.Gen {
"Name": gen.AlphaString(),
"Address": gen.SliceOfN(5, gen.Identifier()),
})
自由に生成方法を指定する
ジェネレーターの Map()
やFlatMap()
は function を渡してジェネレーターを変換することができます。これを利用してstructのプライベートなフィールドのジェネレーター指定も可能です。 別の型への変換にも用いられます。
type User struct {
Name string
age int64 // private
}
userGen := gen.Struct(reflect.TypeOf(&User{}), map[string]gopter.Gen {
"Name": gen.AlphaString(),
}).Map(func (value User) User {
age, _ := gen.Int64Range(18, 24).Sample()
value.age = age.(int64)
return value
})
// 型を変える例
userGen2 := gen.Int64().Map(func(value int64) User {
return User{Name: "fixed", age: value}
})
大量のテストデータでテストを実行する
ジェネレーターを利用してデータを生成しテストします。
arb := arbitrary.DefaultArbitraries()
// ジェネレーターを登録する
arb.RegisterGen(userGen)
prop := gopter.NewProperties(nil)
prop.Property("Test with arbitrary Users", arb.ForAll(func(user User) bool {
return expected == someFunction(user)
}))
// 実行。デフォルトでは100回実行される
prop.Run(gopter.ConsoleReporter(false))
より詳しくは GoDoc や[ソースコード0(https://github.com/leanovate/gopter)を見てください。
所感
コード内でモデルが増えてくると、依存関係も増え構造が複雑になり、テストケースでダミーデータを用意するだけでも一苦労、といった経験はないでしょうか?自動生成はそういったケースで非常に重宝します。
また個人的に、プロパティベースのテストで予期しない境界値の不具合が見つけられ、助けられた経験もあるので、このようなライブラリの存在はありがたいです。ScalaやHaskellと違って型安全ではないため若干辛い部分もありますが、gopterは使いやすいライブラリに仕上がっていると感じました。