Golang Go语言中泛型函数的单元测试实在是太"难"写了
深夜整个人项目,泛型函数单元测试写到吐血了,发帖来吐槽下。单元测试我们知道,一般写法是像下面这样用表驱动测试来写(用到了匿名 struct ):
func Add(a, b int) int {
return a + b
}
// ========== 单元测试分界线 ============
func TestAdd(t *testing.T) {
// 这里定义了一个匿名的 struct ,让代码更简洁容易维护
tests := []struct {
name string
a int
b int
want int
}{
{
name: "ok",
a: 1,
b: 1,
want: 2,
},
{
name: "ok2",
a: 10,
b: 10,
want: 20,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("xxxxxxxxxxxxxxxx")
}
})
}
}
但如果是泛型函数的话,因为目前存在几个问题:
- 匿名函数无法使用泛型
- 匿名结构体无法使用泛型
- 无法在函数里定义非匿名函数
所以泛型函数的单元测试代码就变成了下面这样的写法:
func Add[T constraints.Ordered](a, b T) T {
return a + b
}
// ======== 单元测试分界线 ===========
// 必须在测试函数外单独定义测试用例的结构体
type testCase[T constraints.Ordered] struct {
name string
a T
b T
want T
}
// 同时还必须在测试函数外定义一个执行泛型用例的泛型函数
func runTestCases[T constraints.Ordered](t *testing.T, cases []testCase[T]) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); !reflect.DeepEqual(got, tt.want) {
t.Errorf(“xxxxxxxxxxxxxx”)
}
})
}
}
// 单元测试函数
func TestAdd(t *testing.T) {
intTestCases := []testCase[int]{
{
name: “ok”,
a: 1,
b: 1,
want: 2,
},
{
name: “ok2”,
a: 10,
b: 10,
want: 20,
},
}
strCases := []testCase[string]{
{
name: “ok”,
a: “A”,
b: “B”,
want: “AB”,
},
{
name: “ok2”,
a: “Hello”,
b: “World”,
want: “HelloWorld”,
},
}
runTestCases(t, intTestCases)
runTestCases(t, strCases)
}
也许你会说不就是多定义了个函数还有结构体类型吗,但是我想说的是就是因为这个问题,导致这样子的单元测试代码写起来真的太折磨人了,非常烦人。这段时间我写泛型函数的单元测试都要写吐血了
最重要的是,如果我们在一个文件里定义了多个函数,那么也往往会把他们的单元测试给统一写到同一个 _test.go
文件里。 这种写法导致的结果就是点开一个单元测试代码,里面满眼都是定义在单元测试函数之外的 type xxxTestCase Struct{}
结构体还有 runXXXTestCases[T xxx]()
的泛型函数。可读性和维护起来非常难受。为了可读性解决办法只有一个:给每个泛型函数单独整个 _test.go
文件
嗯,上面就是我的深夜吐槽。不知道今后有没有什么好的工具能结束这种痛苦的写法
Golang Go语言中泛型函数的单元测试实在是太"难"写了
更多关于Golang Go语言中泛型函数的单元测试实在是太"难"写了的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
runTestCases 函数里的循环执行不能被写在 TestAdd 里?还没用过泛型,如果这样写是哪里语法不对么?
更多关于Golang Go语言中泛型函数的单元测试实在是太"难"写了的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
以后用多了肯定有库来简化,就像 java 的 mockito 这种。但是 golang 真的没必要用泛型。
不是,在需要泛型的场景,你不用泛型也得想办法覆盖多种类型情况,复杂度是一样的。在不需要泛型的场景就不要强行用泛型。
golang 用什么泛型啊, 开发组都说不要泛型, 是你们非逼着上的泛型
我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 interface 就好了
type testCase[T any] struct {
name string
a any
b any
want any
}
然后在调用 Add 之前
switch tt.a.(type) {
case string
我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 interface+反射 就好了
type testCase[T any] struct {
name string
a any
b any
want any
}
然后在调用 Add 之前做强转就好了
switch tt.a.(type) {
case string:
Add(reflect.ValueOf(tt.a).String(), reflect.ValueOf(tt.b).String())
}
你明显都没看懂我想说什么,建议重新看一下我的帖子
intTestCases 和 strTestCases 是基于同一个泛型类型实例化出的两个不同的类型的变量,所以如果想在 TestAdd 里跑循环的话,就得分别写两个 for 循环来执行。如果想测的类型多了(float32,float64,int8…),就要写相对应数量的 for 循环。最终肯定是要抽象出一个函数的,但又不能在函数里定义非匿名函数。最终结果就变成了我帖子里这个样子,想更简化的话,得像 ls 说的那样用接口
按照老哥的写法改写了下(实际上其实也用不到反射)的确用不着在测试函数外定义了,但是问题在于每个 case 里都需要重复一遍 t.Run( Add(…)) 的代码,需要测试类型一多就成了这样的画风:
https://gist.github.com/WonderfulSoap/a65747d4296af7ca09e6703ff6e9afbb
如果不介意 case 这一坨的话的确是个不错的解决办法
虽然但是。。。。我这是在讨论泛型函数怎么写单元测试,你们说别用泛型。。。这话题根本对不上啊。
一些工具函数还有数据结构很适合用泛型来写(Add()这个例子很简单所以拿来举例),既然写了函数那肯定要写单元测试的,到头来我帖子里这个问题是躲不开的。
go generate 走起
坚决不用泛型,除非需要用 tmp 生成代码差不多的情况才用泛型
go 的泛型感觉很别扭,很怪,不过也算能解决些问题
go<br>func runTestCases[T constraints.Ordered](t *testing.T, name string, a, b, want T) {<br> <a target="_blank" href="http://t.Run" rel="nofollow noopener">t.Run</a>(name, func(t *testing.T) {<br> if got := Add(a, b); !reflect.DeepEqual(got, want) {<br> t.Errorf("xxxxxxxxxxxxxx")<br> }<br> })<br>}<br><br>func TestAdd(t *testing.T) {<br> runTestCases(t, "ok", 1, 1, 2)<br> runTestCases(t, "ok2", 10, 10, 20)<br> runTestCases(t, "ok", "A", "B", "AB")<br> runTestCases(t, "ok2", "Hello", "World", "HelloWorld")<br>}<br>
这样行么?
写个毛单元测试
现在美国公司就我一个码农
我现在的政策就是反着来 代码尽量冗余 不然改了这个 影响了那个 得不偿失 哈哈哈哈哈
感觉还好,我最近也在尝试写写泛型的链式调用的工具包,只不过 1.18 部分功能没办法实现。
在Go语言中,泛型函数的单元测试确实需要一些额外的思考和技巧,但说它“难”写可能有些夸大其词。以下是一些建议,希望能帮助你更好地编写泛型函数的单元测试:
-
明确泛型约束:在编写泛型函数时,确保你的类型约束是明确且具体的。这有助于你在编写单元测试时,为这些类型约束提供合适的具体类型。
-
使用具体类型进行测试:在单元测试中,你可以通过为泛型参数提供具体类型来实例化泛型函数。这通常是最直接和有效的方法。
-
利用接口:如果你的泛型函数依赖于某些接口方法,那么你可以为这些接口方法编写模拟(mock)对象,并在单元测试中使用这些模拟对象。
-
测试边界情况:对于泛型函数,特别要注意测试边界情况,比如空值、极端值等。这些测试有助于确保你的泛型函数在各种情况下都能正常工作。
-
使用测试框架:Go语言中有许多流行的测试框架,如testify、stretchr/testify等。这些框架提供了丰富的断言和模拟功能,可以大大简化单元测试的编写。
总之,虽然Go语言中的泛型函数单元测试可能需要一些额外的努力,但通过明确泛型约束、使用具体类型进行测试、利用接口、测试边界情况以及使用测试框架等方法,你可以有效地编写出高质量的单元测试。不要害怕挑战,多实践多总结,你会发现它并没有那么“难”。