Skip to content

Instantly share code, notes, and snippets.

@objectx
Last active July 18, 2017 03:39
Show Gist options
  • Save objectx/4568351c30f0f3810dec8a5acd722187 to your computer and use it in GitHub Desktop.
Save objectx/4568351c30f0f3810dec8a5acd722187 to your computer and use it in GitHub Desktop.
gopter 事始め

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 の引数に書くのがつらい…という場合は arbitraryForAllが便利。 渡された関数の引数型から必要な 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 周りが調査不十分なので調べなくては…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment