如何在Go语言中使用反射测试一组函数?

huangapple go评论122阅读模式
英文:

How to test a collection of functions by reflection in Go?

问题

我必须为几个具有相似签名和返回值(一个对象和一个错误)的函数编写单元测试,这些函数必须通过相似的测试条件。我想避免重复编写以下代码:

func TestFunc1(t *testing.T) {
    // 测试返回值
}
func TestFunc2(t *testing.T) {
    // 与Func1相同的测试
}
func TestFunc3(t *testing.T) {
    // 与Func1相同的测试
}
...

(请参阅此Go Playground示例以获取更完整的上下文)
(是的,Go Playground目前不支持go test,只支持go run,并且问题6511正在请求该功能)

你如何使用反射(reflect)来编写只有一个测试的代码,该测试将:

  • 依次调用每个函数?
  • 测试它们的返回值?

我看到了以下内容:

但我缺少一个完整的示例,用于在测试中调用函数并使用返回的值。

英文:

I have to write unit tests for several functions with similar signature and return values (an object and an error), which must pass similar test conditions.
I would like to avoid writing:

<!-- language: lang-go -->

func TestFunc1(t *testing.T) {
	// tests on return values
}
func TestFunc2(t *testing.T) {
	// tests identical for Func1
}
func TestFunc3(t *testing.T) {
	// tests identical for Func1
}
...

(See this go playground example for a more complete context)
(yes, go playground doesn't support yet go test, only go run, and issue 6511 is there to request that feature)

How would you use reflection (reflect package) in order to write only one test which would:

  • call each function in turn?
  • test their return value?

I have seen:

But I miss a complete example for calling functions and using the returned values in a test.

答案1

得分: 7

一旦我理解了一切都必须使用或返回type Value,我就想出了以下解决方案。
关键是使用:

测试代码的主要部分如下:

<!-- language-all: lang-go -->

var funcNames = []string{"Func1", "Func2", "Func3"}

func TestFunc(t *testing.T) {
	stype := reflect.ValueOf(s)
	for _, fname := range funcNames {

		fmt.Println(fname)

		sfunc := stype.MethodByName(fname)
		// 没有参数 =&gt; 空的 Value 切片
		ret := sfunc.Call([]reflect.Value{})

		val := ret[0].Int()
		
		// 对于返回的 err,这会导致 panic
		// err := ret[1].Interface().(error)
                err := ret[1]

		if val &lt; 1 {
			t.Error(fname + " 应该返回正值")
		}
		if err.IsNil() == false {
			t.Error(fname + " 不应该出错")
		}

	}
}

在 Go Playground 中可以看到一个可运行的示例链接


请注意,如果使用不存在的函数名调用该测试函数,会导致 panic。
可以在这个示例中看到。

runtime.panic(0x126660, 0x10533140)
	/tmp/sandbox/go/src/pkg/runtime/panic.c:266 +0xe0
testing.func·005()
	/tmp/sandbox/go/src/pkg/testing/testing.go:383 +0x180
----- stack segment boundary -----
runtime.panic(0x126660, 0x10533140)
	/tmp/sandbox/go/src/pkg/runtime/panic.c:248 +0x160
reflect.flag.mustBe(0x0, 0x13)
	/tmp/sandbox/go/src/pkg/reflect/value.go:249 +0xc0
reflect.Value.Call(0x0, 0x0, 0x0, 0xfeef9f28, 0x0, ...)
	/tmp/sandbox/go/src/pkg/reflect/value.go:351 +0x40
main.TestFunc(0x10546120, 0xe)
	/tmpfs/gosandbox-3642d986_9569fcc1_f443bbfb_73e4528d_c874f1af/prog.go:34 +0x240

Go Playground 可以从 panic 中恢复,但你的测试程序可能无法做到。

这就是为什么我在上面的测试函数中添加了以下代码:

for _, fname := range funcNames {

	defer func() {
		if x := recover(); x != nil {
			t.Error("TestFunc 在", fname, "中发生 panic:", x)
		}
	}()
	fmt.Println(fname)

这样会产生一个更好的输出结果(参见示例):

Func1
Func2
Func3
Func4
--- FAIL: TestFunc (0.00 seconds)
	prog.go:48: Func2 应该返回正值
	prog.go:51: Func3 不应该出错
	prog.go:32: TestFunc 在 Func4 中发生 panic: reflect: call of reflect.Value.Call on zero Value
FAIL
英文:

Once I understood that everything must use or return the type Value, here is what I came up with.
The trick is to use:

Main extract of the test code:

<!-- language-all: lang-go -->

var funcNames = []string{&quot;Func1&quot;, &quot;Func2&quot;, &quot;Func3&quot;}

func TestFunc(t *testing.T) {
	stype := reflect.ValueOf(s)
	for _, fname := range funcNames {

		fmt.Println(fname)

		sfunc := stype.MethodByName(fname)
		// no parameter =&gt; empty slice of Value
		ret := sfunc.Call([]reflect.Value{})

		val := ret[0].Int()
		
		// That would panic for a nil returned err
		// err := ret[1].Interface().(error)
                err := ret[1]

		if val &lt; 1 {
			t.Error(fname + &quot; should return positive value&quot;)
		}
		if err.IsNil() == false {
			t.Error(fname + &quot; shouldn&#39;t err&quot;)
		}

	}
}

See a runnable example in go playground.


Note that if you are calling that test function with a non-existent function name, that will panic.
See that example here.

runtime.panic(0x126660, 0x10533140)
	/tmp/sandbox/go/src/pkg/runtime/panic.c:266 +0xe0
testing.func&#183;005()
	/tmp/sandbox/go/src/pkg/testing/testing.go:383 +0x180
----- stack segment boundary -----
runtime.panic(0x126660, 0x10533140)
	/tmp/sandbox/go/src/pkg/runtime/panic.c:248 +0x160
reflect.flag.mustBe(0x0, 0x13)
	/tmp/sandbox/go/src/pkg/reflect/value.go:249 +0xc0
reflect.Value.Call(0x0, 0x0, 0x0, 0xfeef9f28, 0x0, ...)
	/tmp/sandbox/go/src/pkg/reflect/value.go:351 +0x40
main.TestFunc(0x10546120, 0xe)
	/tmpfs/gosandbox-3642d986_9569fcc1_f443bbfb_73e4528d_c874f1af/prog.go:34 +0x240

Go playground recover from that panic, but your test program might not.

That is why I added to the test function above:

for _, fname := range funcNames {

	defer func() {
		if x := recover(); x != nil {
			t.Error(&quot;TestFunc paniced for&quot;, fname, &quot;: &quot;, x)
		}
	}()
	fmt.Println(fname)

That produces (see example) a much nicer output:

Func1
Func2
Func3
Func4
--- FAIL: TestFunc (0.00 seconds)
	prog.go:48: Func2 should return positive value
	prog.go:51: Func3 shouldn&#39;t err
	prog.go:32: TestFunc paniced for Func4 :  reflect: call of reflect.Value.Call on zero Value
FAIL

huangapple
  • 本文由 发表于 2013年12月29日 17:40:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/20823836.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定