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 · 3478 次点击
    这是一个创建于 1533 天前的主题,其中的信息可能已经有所发展或是发生改变。

    刚刚入坑 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-09-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
    @wzw https://goframe.org/quality/unittest
    三个字,待完善🤣
    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  
       230 天前 via iPhone
    哈哈,偶然发现这个问题,可以试试 https://github.com/xhd2015/xgo
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2906 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 03:14 · PVG 11:14 · LAX 19:14 · JFK 22:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.