英文:
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:
- "How to properly use
.Call
in reflect package, Golang?", using Value.Call - "Selecting a function from a list of functions in Golang"
But I miss a complete example for calling functions and using the returned values in a test.
答案1
得分: 7
一旦我理解了一切都必须使用或返回type Value,我就想出了以下解决方案。
关键是使用:
ValueOf
来获取接收器的值Value.MethodByName
来查找该接收器值的函数Value.IsNil
来测试返回的值是否为nil
。
测试代码的主要部分如下:
<!-- 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)
// 没有参数 => 空的 Value 切片
ret := sfunc.Call([]reflect.Value{})
val := ret[0].Int()
// 对于返回的 err,这会导致 panic
// err := ret[1].Interface().(error)
err := ret[1]
if val < 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:
ValueOf
in order to get a value of the receiverValue.MethodByName
to find a function of that receiver valueValue.IsNil
to test fornil
returned value.
Main extract of the test code:
<!-- 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)
// no parameter => 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 < 1 {
t.Error(fname + " should return positive value")
}
if err.IsNil() == false {
t.Error(fname + " shouldn't err")
}
}
}
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·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("TestFunc paniced for", fname, ": ", 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't err
prog.go:32: TestFunc paniced for Func4 : reflect: call of reflect.Value.Call on zero Value
FAIL
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论