V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
BeautifulSoap
V2EX  ›  Go 编程语言

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

  •  
  •   BeautifulSoap · 2022-03-22 00:40:17 +08:00 · 3810 次点击
    这是一个创建于 738 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    17 条回复    2022-03-22 15:06:38 +08:00
    visitant
        1
    visitant  
       2022-03-22 03:00:25 +08:00
    runTestCases 函数里的循环执行不能被写在 TestAdd 里?还没用过泛型,如果这样写是哪里语法不对么?
    yzbythesea
        2
    yzbythesea  
       2022-03-22 03:08:57 +08:00   ❤️ 1
    以后用多了肯定有库来简化,就像 java 的 mockito 这种。但是 golang 真的没必要用泛型。
    cmdOptionKana
        3
    cmdOptionKana  
       2022-03-22 08:24:57 +08:00   ❤️ 1
    不是,在需要泛型的场景,你不用泛型也得想办法覆盖多种类型情况,复杂度是一样的。在不需要泛型的场景就不要强行用泛型。
    bthulu
        4
    bthulu  
       2022-03-22 08:26:23 +08:00
    golang 用什么泛型啊, 开发组都说不要泛型, 是你们非逼着上的泛型
    SorcererXW
        5
    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
        6
    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
        7
    BeautifulSoap  
    OP
       2022-03-22 08:59:08 +08:00 via Android
    @visitant 你明显都没看懂我想说什么,建议重新看一下我的帖子
    BeautifulSoap
        8
    BeautifulSoap  
    OP
       2022-03-22 09:39:30 +08:00
    @visitant intTestCases 和 strTestCases 是基于同一个泛型类型实例化出的两个不同的类型的变量,所以如果想在 TestAdd 里跑循环的话,就得分别写两个 for 循环来执行。如果想测的类型多了(float32,float64,int8...),就要写相对应数量的 for 循环。最终肯定是要抽象出一个函数的,但又不能在函数里定义非匿名函数。最终结果就变成了我帖子里这个样子,想更简化的话,得像 ls 说的那样用接口
    BeautifulSoap
        9
    BeautifulSoap  
    OP
       2022-03-22 09:54:12 +08:00
    @SorcererXW 按照老哥的写法改写了下(实际上其实也用不到反射)的确用不着在测试函数外定义了,但是问题在于每个 case 里都需要重复一遍 t.Run( Add(...)) 的代码,需要测试类型一多就成了这样的画风:

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

    如果不介意 case 这一坨的话的确是个不错的解决办法
    BeautifulSoap
        10
    BeautifulSoap  
    OP
       2022-03-22 10:02:01 +08:00
    @bthulu
    @visitant
    @yzbythesea
    虽然但是。。。。我这是在讨论泛型函数怎么写单元测试,你们说别用泛型。。。这话题根本对不上啊。
    一些工具函数还有数据结构很适合用泛型来写(Add()这个例子很简单所以拿来举例),既然写了函数那肯定要写单元测试的,到头来我帖子里这个问题是躲不开的。
    tairan2006
        11
    tairan2006  
       2022-03-22 10:08:39 +08:00
    go generate 走起
    lysS
        12
    lysS  
       2022-03-22 10:53:23 +08:00
    坚决不用泛型,除非需要用 tmp 生成代码差不多的情况才用泛型
    yl20181003
        13
    yl20181003  
       2022-03-22 11:02:30 +08:00
    go 的泛型感觉很别扭,很怪,不过也算能解决些问题
    Sunshineplan
        14
    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
        15
    zzzkkk  
       2022-03-22 13:32:03 +08:00
    写个毛单元测试
    现在美国公司就我一个码农
    我现在的政策就是反着来 代码尽量冗余 不然改了这个 影响了那个 得不偿失 哈哈哈哈哈
    anonydmer
        16
    anonydmer  
       2022-03-22 13:36:46 +08:00
    看起来是很麻烦,范型我还没怎么用,但是已经觉得 go 的单元测试写起来很麻烦了
    wwaayyaa
        17
    wwaayyaa  
       2022-03-22 15:06:38 +08:00
    感觉还好,我最近也在尝试写写泛型的链式调用的工具包,只不过 1.18 部分功能没办法实现。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1002 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 22:20 · PVG 06:20 · LAX 15:20 · JFK 18:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.