go 泛型函数的单元测试实在是太"难"写了 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
BeautifulSoap

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

  •  
  •   BeautifulSoap 2022 年 3 月 22 日 4689 次点击
    这是一个创建于 1495 天前的主题,其中的信息可能已经有所发展或是发生改变。

    深夜整个人项目,泛型函数单元测试写到吐血了,发帖来吐槽下。单元测试我们知道,一般写法是像下面这样用表驱动测试来写(用到了匿名 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 年 3 月 22 日
    runTestCases 函数里的循环执行不能被写在 TestAdd 里?还没用过泛型,如果这样写是哪里语法不对么?
    yzbythesea
        2
    yzbythesea  
       2022 年 3 月 22 日   1
    以后用多了肯定有库来简化,就像 java 的 mockito 这种。但是 golang 真的没必要用泛型。
    cmdOptionKana
        3
    cmdOptionKana  
       2022 年 3 月 22 日   1
    不是,在需要泛型的场景,你不用泛型也得想办法覆盖多种类型情况,复杂度是一样的。在不需要泛型的场景就不要强行用泛型。
    bthulu
        4
    bthulu  
       2022 年 3 月 22 日
    golang 用什么泛型啊, 开发组都说不要泛型, 是你们非逼着上的泛型
    SorcererXW
        5
    SorcererXW  
       2022 年 3 月 22 日
    我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 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 年 3 月 22 日
    我理解的泛型的意义在于提高代码复用率,相比反射性能更好。这两点在单元测试里面似乎没有那么重要,单测里面可能直接用 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 年 3 月 22 日 via Android
    @visitant 你明显都没看懂我想说什么,建议重新看一下我的帖子
    BeautifulSoap
        8
    BeautifulSoap  
    OP
       2022 年 3 月 22 日
    @visitant intTestCases 和 strTestCases 是基于同一个泛型类型实例化出的两个不同的类型的变量,所以如果想在 TestAdd 里跑循环的话,就得分别写两个 for 循环来执行。如果想测的类型多了(float32,float64,int8...),就要写相对应数量的 for 循环。最终肯定是要抽象出一个函数的,但又不能在函数里定义非匿名函数。最终结果就变成了我帖子里这个样子,想更简化的话,得像 ls 说的那样用接口
    BeautifulSoap
        9
    BeautifulSoap  
    OP
       2022 年 3 月 22 日
    @SorcererXW 按照老哥的写法改写了下(实际上其实也用不到反射)的确用不着在测试函数外定义了,但是问题在于每个 case 里都需要重复一遍 t.Run( Add(...)) 的代码,需要测试类型一多就成了这样的画风:

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

    如果不介意 case 这一坨的话的确是个不错的解决办法
    BeautifulSoap
        10
    BeautifulSoap  
    OP
       2022 年 3 月 22 日
    @bthulu
    @visitant
    @yzbythesea
    虽然但是。。。。我这是在讨论泛型函数怎么写单元测试,你们说别用泛型。。。这话题根本对不上啊。
    一些工具函数还有数据结构很适合用泛型来写(Add()这个例子很简单所以拿来举例),既然写了函数那肯定要写单元测试的,到头来我帖子里这个问题是躲不开的。
    tairan2006
        11
    tairan2006  
       2022 年 3 月 22 日
    go generate 走起
    lysS
        12
    lysS  
       2022 年 3 月 22 日
    坚决不用泛型,除非需要用 tmp 生成代码差不多的情况才用泛型
    yl20181003
        13
    yl20181003  
       2022 年 3 月 22 日
    go 的泛型感觉很别扭,很怪,不过也算能解决些问题
    Sunshineplan
        14
    Sunshineplan  
       2022 年 3 月 22 日
    ```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 年 3 月 22 日
    写个毛单元测试
    现在美国公司就我一个码农
    我现在的政策就是反着来 代码尽量冗余 不然改了这个 影响了那个 得不偿失 哈哈哈哈哈
    anonydmer
        16
    anonydmer  
       2022 年 3 月 22 日
    看起来是很麻烦,范型我还没怎么用,但是已经觉得 go 的单元测试写起来很麻烦了
    wwaayyaa
        17
    wwaayyaa  
       2022 年 3 月 22 日
    感觉还好,我最近也在尝试写写泛型的链式调用的工具包,只不过 1.18 部分功能没办法实现。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2995 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 96ms UTC 07:26 PVG 15:26 LAX 00:26 JFK 03:26
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86