V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
pkoukk

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

  •  
  •   pkoukk · Sep 4, 2020 · 4238 views
    This topic created in 2062 days ago, the information mentioned may be changed or developed.

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

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

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

    举个例子:

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

    原因在于,如果资源 mock,可以不能测试到使用资源的错误,比如一条 SQL 语句其实错了,但是不能发现
    crclz
        11
    crclz  
       Sep 4, 2020
    java 和 c#的特性才刚刚足够使用。对于 golang 这种丐版的语言,你还期望什么?
    wzw
        12
    wzw  
       Sep 4, 2020
    @pkoukk #9 没文档而已吧, 你看看例子
    limboMu
        13
    limboMu  
       Sep 4, 2020
    跑单元测试说明需要 mock 说明你的方法本身就不是纯函数,总归是需要一个环境(ctx)执行的.
    rim99
        14
    rim99  
       Sep 5, 2020
    看看楼上的讨论,也有了些结论:
    1. 对外部环境、框架有严重依赖的代码,应该从核心业务逻辑中抽离出去,用集成测试覆盖。
    2. 核心业务内部使用接口完成逻辑的组织,用单元测试覆盖。
    js2854
        15
    js2854  
       Sep 10, 2020
    可以使用 gomonkey
    github.com/agiledragon/gomonkey
    js2854
        16
    js2854  
       Sep 10, 2020
    xhd2015
        17
    xhd2015  
       Mar 30, 2024 via iPhone
    哈哈,偶然发现这个问题,可以试试 https://github.com/xhd2015/xgo
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5650 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 75ms · UTC 06:51 · PVG 14:51 · LAX 23:51 · JFK 02:51
    ♥ Do have faith in what you're doing.