How to test a collection of functions by reflectio

2020-03-15 05:31发布

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:

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条回答
戒情不戒烟
2楼-- · 2020-03-15 06:06

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:

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
查看更多
登录 后发表回答