go 泛型函数的单元测试实在是太"难"写了

2022-03-22 00:40:17 +08:00
 BeautifulSoap

深夜整个人项目,泛型函数单元测试写到吐血了,发帖来吐槽下。单元测试我们知道,一般写法是像下面这样用表驱动测试来写(用到了匿名 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 文件

嗯,上面就是我的深夜吐槽。不知道今后有没有什么好的工具能结束这种痛苦的写法

4049 次点击
所在节点    Go 编程语言
17 条回复
visitant
2022-03-22 03:00:25 +08:00
runTestCases 函数里的循环执行不能被写在 TestAdd 里?还没用过泛型,如果这样写是哪里语法不对么?
yzbythesea
2022-03-22 03:08:57 +08:00
以后用多了肯定有库来简化,就像 java 的 mockito 这种。但是 golang 真的没必要用泛型。
cmdOptionKana
2022-03-22 08:24:57 +08:00
不是,在需要泛型的场景,你不用泛型也得想办法覆盖多种类型情况,复杂度是一样的。在不需要泛型的场景就不要强行用泛型。
bthulu
2022-03-22 08:26:23 +08:00
golang 用什么泛型啊, 开发组都说不要泛型, 是你们非逼着上的泛型
SorcererXW
2022-03-22 08:43:01 +08:00
我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 interface 就好了

type testCase[T any] struct {
name string
a any
b any
want any
}

然后在调用 Add 之前

switch tt.a.(type) {
case string
SorcererXW
2022-03-22 08:44:56 +08:00
我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 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())
}
BeautifulSoap
2022-03-22 08:59:08 +08:00
@visitant 你明显都没看懂我想说什么,建议重新看一下我的帖子
BeautifulSoap
2022-03-22 09:39:30 +08:00
@visitant intTestCases 和 strTestCases 是基于同一个泛型类型实例化出的两个不同的类型的变量,所以如果想在 TestAdd 里跑循环的话,就得分别写两个 for 循环来执行。如果想测的类型多了(float32,float64,int8...),就要写相对应数量的 for 循环。最终肯定是要抽象出一个函数的,但又不能在函数里定义非匿名函数。最终结果就变成了我帖子里这个样子,想更简化的话,得像 ls 说的那样用接口
BeautifulSoap
2022-03-22 09:54:12 +08:00
@SorcererXW 按照老哥的写法改写了下(实际上其实也用不到反射)的确用不着在测试函数外定义了,但是问题在于每个 case 里都需要重复一遍 t.Run( Add(...)) 的代码,需要测试类型一多就成了这样的画风:

https://gist.github.com/WonderfulSoap/a65747d4296af7ca09e6703ff6e9afbb

如果不介意 case 这一坨的话的确是个不错的解决办法
BeautifulSoap
2022-03-22 10:02:01 +08:00
@bthulu
@visitant
@yzbythesea
虽然但是。。。。我这是在讨论泛型函数怎么写单元测试,你们说别用泛型。。。这话题根本对不上啊。
一些工具函数还有数据结构很适合用泛型来写(Add()这个例子很简单所以拿来举例),既然写了函数那肯定要写单元测试的,到头来我帖子里这个问题是躲不开的。
tairan2006
2022-03-22 10:08:39 +08:00
go generate 走起
lysS
2022-03-22 10:53:23 +08:00
坚决不用泛型,除非需要用 tmp 生成代码差不多的情况才用泛型
yl20181003
2022-03-22 11:02:30 +08:00
go 的泛型感觉很别扭,很怪,不过也算能解决些问题
Sunshineplan
2022-03-22 11:52:13 +08:00
```go
func runTestCases[T constraints.Ordered](t *testing.T, name string, a, b, want T) {
t.Run(name, func(t *testing.T) {
if got := Add(a, b); !reflect.DeepEqual(got, want) {
t.Errorf("xxxxxxxxxxxxxx")
}
})
}

func TestAdd(t *testing.T) {
runTestCases(t, "ok", 1, 1, 2)
runTestCases(t, "ok2", 10, 10, 20)
runTestCases(t, "ok", "A", "B", "AB")
runTestCases(t, "ok2", "Hello", "World", "HelloWorld")
}
```

这样行么?
zzzkkk
2022-03-22 13:32:03 +08:00
写个毛单元测试
现在美国公司就我一个码农
我现在的政策就是反着来 代码尽量冗余 不然改了这个 影响了那个 得不偿失 哈哈哈哈哈
anonydmer
2022-03-22 13:36:46 +08:00
看起来是很麻烦,范型我还没怎么用,但是已经觉得 go 的单元测试写起来很麻烦了
wwaayyaa
2022-03-22 15:06:38 +08:00
感觉还好,我最近也在尝试写写泛型的链式调用的工具包,只不过 1.18 部分功能没办法实现。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/841982

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX