How to test a collection of functions by reflectio

2020-03-15 06:15发布

问题:

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:

  • "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:

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 receiver
  • Value.MethodByName to find a function of that receiver value
  • Value.IsNil to test for nil returned value.

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