Go は吊しで testing/quick
を property based testing 用に持っているけど、チト薄い感じだったので gopter
を試す…
gopter
(というか property based testing) の基本は、値の generator が作った値を不変条件を表す関数に流し込んで OK/NG を判定する事。値の生成は gopter
がよしなにやってくれる(generator で頑張れば、失敗した後に失敗した事例の絞り込み(property test 界隈では Shrinking と呼称される)も出来る)
// Borrowed from official gopter documentation.
func TestSqrt(t *testing.T) {
// Property holder
properties := gopter.NewProperties(nil)
// Add 1st property
properties.Property("greater one of all greater one", prop.ForAll(
func(v float64) bool {
return math.Sqrt(v) >= 1
},
// Generates value X | 1 <= X < math.MaxFloat64
gen.Float64Range(1, math.MaxFloat64),
))
// Add 2nd property
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.Max)Float64),
))
// Run tests (under testing environment)
properties.TestingRun(t)
}
generator は基本型に対するものは網羅的にあるのでそれを使う。test に失敗した時の確認に便利な様に generator には .WithLabel(s string)
で名前が付く
基本型で足りない時は combinator 各種を組み合わせて新たに generator を作る
func TestToBoolean_Falsy(t *testing.T) {
generator := gopter.CombineGens(
gen.OneConstOf("false", "f", "no", "n", "off", "0"),
gen.AlphaString(),
)
condition := func(s string) bool {
return !ToBoolean(s)
}
Convey(`Falsy values should evaluate to false`, t, func() {
So(condition, convey.ShouldSucceedForAll,
generator.FlatMap(genVariants, reflect.TypeOf("")).WithLabel("falsy"))
})
}
// genVariants constructs a generator for testing `ToBoolean`.
func genVariants(arg interface{}) gopter.Gen {
args := arg.([]interface{})
s := args[0].(string)
t := args[1].(string)
return gen.OneConstOf(s, strings.ToUpper(s), strings.Title(s),
fmt.Sprintf("%s %s", s, t),
fmt.Sprintf("%s %s", strings.ToUpper(s), t),
fmt.Sprintf("%s %s", strings.Title(s), t),
)
}
上記の例は goconvey の中で property test を走らせている。
少し悩んだのは combiner に渡ってくる引数が arg interface{}
だったりする所。判ってしまえば『そんな物かな?』とは思えるのだけど🤔
property を表す関数の引数が多くて、一々 .ForAll
の引数に書くのがつらい…という場合は arbitrary
の ForAll
が便利。 渡された関数の引数型から必要な generator を導出して使ってくれる。arbitrary が知らない型の generator は自分で作って RegisterGen
すれば良い。
func TestVariable(t *testing.T) {
arbitraries := arbitrary.DefaultArbitraries()
arbitraries.RegisterGen(gen.Identifier().Map(func(arg interface{}) PlatformID {
v := arg.(string)
return PlatformID(v)
}))
arbitraries.RegisterGen(gen.SliceOf(gen.Identifier()).Map(func(arg interface{}) *PlatformIDSet {
var result PlatformIDSet
for _, v := range arg.([]string) {
result.Add(PlatformID(v))
}
return &result
}))
condition := func(name string, value string, platforms *PlatformIDSet, target string, build string) bool {
v := Variable{
Name: name,
Value: value,
Platforms: platforms,
Target: target,
Build: build,
}
b, err := yaml.Marshal(&v)
if err != nil {
t.Logf("%v", err)
return false
}
// t.Logf("%s\n", string(b))
var vv Variable
err = yaml.Unmarshal(b, &vv)
if err != nil {
t.Logf("%v", err)
return false
}
//t.Logf("Platform: %v", vv.platforms)
return v.Equals(&vv)
}
Convey(`Marshal then Unmarshal should return to original`, t, func() {
So(condition, convey.ShouldSucceedForAll, arbitraries)
})
}
gopter の convey.ShouldSucceedForAll
は引数の並びと型を見て、gopter.Arbitraries
が混ざっている場合は 動作を変えている ってのが ShouldSuccessForAll
に一切説明が無くて、思わず Use the source, Luke したのはしみつだ…
Shrinker 周りが調査不十分なので調べなくては…