go 有没有什么优雅的办法可以进行单元测试? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
pkoukk
V2EX    Go 编程语言

go 有没有什么优雅的办法可以进行单元测试?

  •  
  •   pkoukk 2020-09-04 14:16:12 +08:00 4002 次点击
    这是一个创建于 1922 天前的主题,其中的信息可能已经有所发展或是发生改变。

    刚刚入坑 golang 没多久,用 go 写了一个小的项目,逐渐感受到了 go 的特性和优点
    为了增加项目的可靠性,想写一点单元测试,但是发现了重重阻碍,主要是在 mock 的时候实在是太复杂了
    个人体验上感觉,无法做到无侵入性的 mock 某些 func 或者 struct 。
    下面写一下自己的做法,不知道是因为我原本代码结构设计的就不对还是 mock 的姿势不对,希望大家指正

    函数 mock 。

    在一般的使用中假设是这样的

    func BaseFunc() { info := getInfo("123") fmt.Printf(info) } func getInfo(name string) string { return name + ".cn" } func usage() { BaseFunc() } 

    如果我需要 mock,就得

    type getInfoFunc func(string) string func BaseWithMock(getInfoV getInfoFunc) { info := getInfoV("123") fmt.Printf(info) } func mockGetInfo(name string) string { return name + ".com" } func usage() { // 正常调用 BaseWithMock(getInfo) // mock BaseWithMock(mockGetInfo) } 

    那么就存在问题了,如果我的 baseFunc 当中有很多数据库或者 API 接口,在单元测试的时候我需要 mock 他们的数据,
    我就必须要定义很多个 type,然后在 BaseFunc 的参数里传进来么?感觉这么做很不优雅。

    如果试图去 mock 一个对象,我感觉就更复杂了..
    以下是某种简化过的场景..
    假设 ServiceRecord 代表一系列数据库和 API 等数据操作
    Service 则代表具体处理的对象,那么如果 Service 需要通过 ServiceRecord 读取某些基础信息的场景。
    一般情况下,我是这样写的

    type ServiceRecord struct { Name string Fields map[string]string } func (s *ServiceRecord) LoadFields() { // some database work result := map[string]string{ "name": "jack", "address": "no.1 jack street", "remark": "none", } s.Fields = result } type Service struct { Name string Owner string } func (s *Service) ReadMoreInfo() { r := &ServiceRecord{Name: s.Name} r.LoadFields() s.Owner = r.Fields["name"] } func usage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfo() fmt.Print(s.Owner) } 

    如果需要进行单元测试,我们需要 mock 掉数据层,也就是 ServiceRecord 这个对象。 一般是通过 Interface 来实现这件事情。

    type ServiceRecordInterface interface { LoadFields() // 因为 Interface 本身不包含数据,所以原来的所有直接访问属性的地方,都必须使用函数来实现 GetField(string) string } func (s *ServiceRecord) GetField(fieldName string) string { return s.Fields[fieldName] } // 为了 Mock,需要通过参数把接口传进来 func (s *Service) ReadMoreInfo(serviceRecord ServiceRecordInterface) { serviceRecord.LoadFields() s.Owner = serviceRecord.GetField("name") } // 正常调用时 func usage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfoForMock(&ServiceRecord{Name: serviceName}) fmt.Printf(s.Owner) } // mock 对象 type ServiceRecordMock struct { ServiceRecord } // mock 掉具体的函数实现 func (srm *ServiceRecordMock) LoadFields() { result := map[string]string{ "name": "tony", "address": "no.1 tony street", "remark": "none", } srm.Fields = result } // mock 时 func mockUsage(serviceName string) { s := &Service{Name: serviceName} s.ReadMoreInfoForMock(&ServiceRecordMock{ServiceRecord{Name: serviceName}}) fmt.Printf(s.Owner) } 

    可以看出这仍然对原来的代码产生了很大的影响,为了满足可 mock,必须要声明一个接口,而且必须把这个接口抽离出来,作为参数注入到调用对象里面去。
    让我觉得很尴尬的是,采用接口之后,必须通过函数去 get 或者 set 一个属性,感觉很不优雅,产生了很多没有必要的垃圾代码。

    17 条回复    2024-03-30 14:02:37 +08:00
    wangsyi13
        1
    wangsyi13  
       2020-09-04 15:41:54 +08:00
    关注一下,我也有这个问题,我最近转 go,接触新项目,想写些单元测试,发现只要涉及业务的就关联太多,很难实现,可能项目设计之初就没有考虑单元测试,但是如果考虑的话,项目结构设计应该遵循哪些原则呢?
    abser
        2
    abser  
       2020-09-04 15:50:06 +08:00
    单元测试需要 mock 的很少, 一般直接测试函数输入输出, 直接给用例测.
    pkoukk
        3
    pkoukk  
    OP
       2020-09-04 16:12:10 +08:00
    @abser 那有数据或者外部依赖的函数怎么办呢?不测了么?...
    cloudzhou
        4
    cloudzhou  
       2020-0-04 16:17:39 +08:00
    关于 Go 的单元测试,一下是我的几个思考点:

    1. 基于 interface 的 mock,这是面向接口可插拔,已经很成熟,就是实现对应的 mock 接口实现注入,具体方法 mock,具体不说了

    2. 面向方法的 mock,使用 context 模型,进行 mock 方法注入

    举个例子:

    https://play.golang.org/p/z5RDSVcTSWD
    cloudzhou
        5
    cloudzhou  
       2020-09-04 16:18:28 +08:00
    @pkoukk 以上,可以做到针对函数精确的 mock,按需实现
    vvmint233
        6
    vvmint233  
       2020-09-04 16:59:17 +08:00
    面向数据库的 mock 的话直接 mock 掉底层的 db 对象, 底层的 db 对象不是 interface 的话我一般本地起一个数据库或者 sqlite3 做 mock 数据源, 这样就只需要构造数据或者 sql 文件, 缺点是对 ci/cd 不友好. 不过你可以检查你 io 部分的函数有没有问题, 逻辑部分的函数直接构造输入输出 Test 就好了
    wzw
        7
    wzw  
       2020-09-04 18:05:49 +08:00 via iPhone
    看看 goframe
    pkoukk
        8
    pkoukk  
    OP
       2020-09-04 18:12:10 +08:00   1
    @vvmint233 是的..只不过这么做更像是跑在本地的集成测试了,不那么像单测了
    pkoukk
        9
    pkoukk  
    OP
       2020-09-04 18:14:11 +08:00
    cloudzhou
        10
    cloudzhou  
       2020-09-04 18:23:57 +08:00
    @vvmint233 @pkoukk 资源性的依赖,我更倾向不要 mock 资源,而是按需构建资源,其实代价很小
    我做过一个测试项目,就是将所有资源 Docker 化,并且做好初始,结束动作
    Mysql / Redis 都是调用 Docker,“资源即服务”,甚至可以构建一个 pool 来重复利用

    原因在于,如果资源 mock,可以不能测试到使用资源的错误,比如一条 SQL 语句其实错了,但是不能发现
    crclz
        11
    crclz  
       2020-09-04 18:41:25 +08:00
    java 和 c#的特性才刚刚足够使用。对于 golang 这种丐版的语言,你还期望什么?
    wzw
        12
    wzw  
       2020-09-04 19:02:12 +08:00
    @pkoukk #9 没文档而已吧, 你看看例子
    limboMu
        13
    limboMu  
       2020-09-04 19:43:08 +08:00
    跑单元测试说明需要 mock 说明你的方法本身就不是纯函数,总归是需要一个环境(ctx)执行的.
    rim99
        14
    rim99  
       2020-09-05 18:52:40 +08:00
    看看楼上的讨论,也有了些结论:
    1. 对外部环境、框架有严重依赖的代码,应该从核心业务逻辑中抽离出去,用集成测试覆盖。
    2. 核心业务内部使用接口完成逻辑的组织,用单元测试覆盖。
    js2854
        15
    js2854  
       2020-09-10 13:17:26 +08:00
    可以使用 gomonkey
    github.com/agiledragon/gomonkey
    js2854
        16
    js2854  
       2020-09-10 13:19:44 +08:00
    xhd2015
        17
    xhd2015  
       2024-03-30 14:02:37 +08:00 via iPhone
    哈哈,偶然发现这个问题,可以试试 https://github.com/xhd2015/xgo
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5208 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 08:49 PVG 16:49 LAX 00:49 JFK 03:49
    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