Golang Go语言中泛型函数的单元测试实在是太"难"写了

发布于 1周前 作者 songsunli 来自 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")
			}
		})
	}
}

但如果是泛型函数的话,因为目前存在几个问题:

  1. 匿名函数无法使用泛型
  2. 匿名结构体无法使用泛型
  3. 无法在函数里定义非匿名函数

所以泛型函数的单元测试代码就变成了下面这样的写法:


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

18 回复

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>

这样行么?

写个毛单元测试
现在美国公司就我一个码农
我现在的政策就是反着来 代码尽量冗余 不然改了这个 影响了那个 得不偿失 哈哈哈哈哈

看起来是很麻烦,范型我还没怎么用,但是已经觉得 go 的单元测试写起来很麻烦了

感觉还好,我最近也在尝试写写泛型的链式调用的工具包,只不过 1.18 部分功能没办法实现。

在Go语言中,泛型函数的单元测试确实需要一些额外的思考和技巧,但说它“难”写可能有些夸大其词。以下是一些建议,希望能帮助你更好地编写泛型函数的单元测试:

  1. 明确泛型约束:在编写泛型函数时,确保你的类型约束是明确且具体的。这有助于你在编写单元测试时,为这些类型约束提供合适的具体类型。

  2. 使用具体类型进行测试:在单元测试中,你可以通过为泛型参数提供具体类型来实例化泛型函数。这通常是最直接和有效的方法。

  3. 利用接口:如果你的泛型函数依赖于某些接口方法,那么你可以为这些接口方法编写模拟(mock)对象,并在单元测试中使用这些模拟对象。

  4. 测试边界情况:对于泛型函数,特别要注意测试边界情况,比如空值、极端值等。这些测试有助于确保你的泛型函数在各种情况下都能正常工作。

  5. 使用测试框架:Go语言中有许多流行的测试框架,如testify、stretchr/testify等。这些框架提供了丰富的断言和模拟功能,可以大大简化单元测试的编写。

总之,虽然Go语言中的泛型函数单元测试可能需要一些额外的努力,但通过明确泛型约束、使用具体类型进行测试、利用接口、测试边界情况以及使用测试框架等方法,你可以有效地编写出高质量的单元测试。不要害怕挑战,多实践多总结,你会发现它并没有那么“难”。

回到顶部