We were brainstorming possible ways of testing generic functions using table driven tests. It seems pretty complicated at the first glance.
What we wanted to achieve is to have a field in the test table struct that can be of whatever type that is accepted by the generic function. It seems however, that you can't use any
interface for that.
We came up with the following solution:
Example function:
func PtrTo[T any](t T) *T {
return &t
}
Example test:
func TestPtrTo(t *testing.T) {
string1 := "abcd"
trueVar := true
testCases := []struct {
testDescription string
input interface{}
expectedOutput interface{}
}{
{
testDescription: "string",
input: string1,
expectedOutput: &string1,
},
{
testDescription: "bool",
input: trueVar,
expectedOutput: &trueVar,
},
}
for _, testCase := range testCases {
t.Run(testCase.testDescription, func(t *testing.T) {
switch concreteTypeInput := testCase.input.(type) {
case string:
output := PtrTo(concreteTypeInput)
assert.Equal(t, testCase.expectedOutput, output)
case bool:
output := PtrTo(concreteTypeInput)
assert.Equal(t, testCase.expectedOutput, output)
default:
t.Error("Unexpected type. Please add the type to the switch case")
}
})
}
}
It doesn't really feel optimal, though.
What do you think of this solution?
Do you see any other alternatives?
CodePudding user response:
It doesn't make sense to table-test generics the way you describe.
The code is generic: if it compiles in the first place, then you have the compile-time guarantee that it will behave exactly the same for all values of T
that satisfy the constraint(s).
In other words, it's pointless to test that the return type of PtrTo
is *bool
when T
is bool
. The compiler already type-checked it for you.
It makes sense to test different input types only if:
- there is type-based conditional logic within the function body, i.e. a type switch, so that the function output could effectively differ
T
is constrained by a union and the function body makes use of operators that have different semantics based on the type, e.g.
When you have broad constraints like any
and the semantics of the function body are the same for all T
s, just test it with a random type and call it a day.
CodePudding user response:
A templated helper function could give you the semantics I think you're looking for:
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
)
type testCase[T any] struct {
desc string
in T
want *T
}
func ptrToTest[T any](t *testing.T, tc testCase[T]) {
t.Helper()
t.Run(tc.desc, func(t *testing.T) {
got := PtrTo(tc.in)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("got %v, want %v", got, tc.want)
}
})
}
func TestPtrTo(t *testing.T) {
string1 := "abcd"
ptrToTest(t, testCase[string]{
desc: "string",
in: string1,
want: &string1,
})
trueVar := true
ptrToTest(t, testCase[bool]{
desc: "bool",
in: trueVar,
want: &trueVar,
})
}
That being said, I agree with @blackgreen that the PtrTo
function is too trivial for a test like this to be meaningful. Hopefully this is useful for more complicated logic!