百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

玩转Go单元测试,你只需要掌握这5点

toyiye 2024-06-21 12:00 20 浏览 0 评论

最近在给项目代码完善单元测试,发现go语言单元测试相关的资料都是零零散散的,所以在这儿整理总结一下。项目中使用的是goconvey+monkey+sqlmock (项目的web框架为gin, 持久层框架为gorm), 使用时也碰到一些坑,也会在这篇文章中做一些相关的记录。 文章大约4200字,囿于篇幅,很多地方都是一笔带过,不过在每一部分之后提供了一些笔者读过觉得不错的资料的链接,大家可以根据需要查看。

1. Go对单元测试的原生支持

1.1 testing——Go内置的单元测试库。

要编写一个新的测试,需要创建一个以 _test.go 结尾的文件,该文件包含 TestXxx 函数。 将该文件放在与被测试的包相同的包中。

通过 go test 命令,能够自动执行如下形式的任何函数:

func TestXxx(*testing.T)

注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母(一般接被测试函数名字,不强求)。传递给测试函数的参数是 *testing.T 类型。它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。

详情参见:The-Golang-Standard-Library-by-Example

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.1.html

1.2 TestMain

在写测试时,有时需要在测试之前或之后进行额外的设置(setup)或拆卸(teardown);有时,测试还需要控制在主线程上运行的代码。为了支持这些需求,testing 提供了 TestMain 函数:

func TestMain(m *testing.M)

如果测试文件中包含该函数,那么生成的测试将调用 TestMain(m),而不是直接运行测试。

TestMain 运行在主 goroutine 中, 可以在调用 m.Run 前后做任何设置和拆卸。注意,在 TestMain 函数的最后,应该使用 m.Run 的返回值作为参数调用 os.Exit。

详情参见:TestMain

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.5.html#testmain

1.3 httptest——HTTP测试辅助工具

Go 标准库专门提供了 httptest 包专门用于进行 http Web 开发测试。

httptest包最关键的是提供了一个 http.ReponseWriter接口的实现结构:httptest.ReponseRecorder,通过它可以得到一个http.ReponseWriter,并以此来接收服务器返回的响应包。

详情参见:httptest - HTTP 测试辅助工具

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.6.html

1.4 测试覆盖率

Go 从 1.2 开始,引入了测试覆盖率的支持,使用的是 cover 相关的工具(go test -cover、go tool cover)。

详情参见:The cover story

https://blog.golang.org/cover

2. 断言库

Go标准包里并没有断言库,但是不使用断言库进行结果校验的话,测试代码将会变得非常臃肿,可读性和可维护性都会很差。不过好在有第三方框架可以让我们使用。

2.1 testify

github地址:https://github.com/stretchr/testify

特性:

  • 在提供断言功能之外,还提供了mock的功能
  • suite包可以给每个测试用例进行前置操作和后置操作的功能(例如初始化和清空数据库)

2.2 gocheck

godoc地址:https://godoc.org/gopkg.in/check.v1

特性:

  • 丰富了单元测试常用的 assert 断言,判断动词deep multi-type 对比,字符串比较以及正则匹配
  • 测试用例组织集合方面按suite组织测试用例,支持suite级别的 setup() 和 teardown()
  • 对于临时文件支持创建、删除临时文件和目录。

详情参见:gocheck 使用介绍

https://zhuanlan.zhihu.com/p/45570168

2.3 goconvey

github地址: https://github.com/smartystreets/goconvey

特性:

  • 直接集成go test
  • 可以管理和运行测试用例
  • 提供了丰富的断言函数
  • 支持很多 Web 界面特性(通过http://localhost:8080访问)
  • 设置界面主题
  • 查看完整的测试结果
  • 使用浏览器提醒
  • 自动检测代码变动并编译测试
  • 半自动化书写测试用例:http://localhost:8080/composer.html
  • 查看测试覆盖率:http://localhost:8080/reports/
  • 临时屏蔽某个包的编译测试

详情参见:GoConvey框架使用指南

https://www.jianshu.com/p/e3b2b1194830

2.4 比较

其实gocheck我没怎么用过,只是当时调研的时候看到了,在断言方面看起来和其他的差不多。

testify和goconvey都有尝试,最后采用的是goconvey,所以对goconvey更熟悉一点。

  • testify
  • star数和活跃度较高:这个其实挺重要的,因为很多人都在用这个框架的话,这个框架会得到更好的完善和发展,也会更有生命力。在使用时碰到的问题也可以很方便的在issue中找到答案;
  • testify类似于gocheck和gomock的结合体,但是其mock使用并不是很方便,所以建议还是使用专门的mock框架。
  • goconvey
  • 可以管理和运行测试用例,通过嵌套来体现测试用例之间的关系。这是我当时选择使用goconvey的一个很重要原因,它可以将测试代码组织得更富逻辑性和结构化,提高了测试代码的可读性和可维护性
  • 支持在web界面进行自动化编译测试。之前在油管上看到一个博主通过web界面来半自动化生成测试代码。不过我实际并没有使用这个功能,感觉这个功能适合逻辑简单/清楚的代码。

3 mock/stub方案

3.1 识别依赖

普遍来说,我们遇到最常见的依赖无非下面几种:

  • 网络依赖——函数执行依赖于网络请求,比如第三方http-api,rpc服务,消息队列等
  • 数据库依赖
  • I/O依赖(文件)
  • 还未开发完成的功能模块

3.2 mock和stub的区别

这个话题也算是老生常谈了。几句话很难解释清楚,有兴趣可以阅读Martin Fowler的文章。

stub本质上是对真实对象的一个模拟,比如调用者需要一个值,那就让stub输出一个值,如果调用者需要传递一个值给stub,那就在stub中定义一个方法接受该参数,相当于“依赖部分”的一个简化实现。mock则是在程序代码中向被测试代码注入“依赖部分”,模拟出函数调用返回的结果。

个人认为两者最大的区别在于依赖对象是否和被测对象有交互,从结果来看,stub不会使测试失败,它只是为被测对象提供依赖的对象,并不改变测试结果,而mock则会根据不同的交互测试要求,很可能会更改测试的结果。stub是state-based,关注的是输入和输出。mock是interaction-based,关注的是交互过程。

mock和stub还有一个重要的区别就是expectiation。对于mock来说,expectiation是重中之重:我们期待方法有没有被调用,期待适当的参数,期待调用的次数,甚至期待多个mock之间的调用顺序。所有的一切期待都是事先准备好,在测试过程中和测试结束后验证是否和预期的一致。而对于stub,通常都不会关注expectiation,没有任何代码来帮助判断这个stub类是否被调用。虽然理论上某些stub实现也可以通过自己编码的方式增加对expectiation的内容,比如增加一个计数器,每次调用+1之类,但是实际上极少这样做。

在Go中,如果要用stub,那将是侵入式的,必须将代码设计成可以用stub方法替换的形式。为了测试,需要专门用一个全局变量 来保存具有外部依赖的方法。然而在不提倡使用全局变量的Go语言当中,这显然是不合适的。所以,并不提倡这种Stub方式。

但其实这两种方法并不是割裂的,例如像下文提到的gomock框架除了像其名字一样可以mock对象以外,还提供了stub的功能。软件工程没有银弹,我们需要根据合适的场景选用合适的方法,甚至可以结合多种方法使用。

详情参见:Mocks Aren't Stubs(Martin Fowler)

https://martinfowler.com/articles/mocksArentStubs.html

以及 中文翻译

https://www.cnblogs.com/anf/archive/2006/03/27/360248.html

3.3 gostub

github地址:https://github.com/prashantv/gostub

特性:

  • 可以为全局变量、函数、过程打桩
  • 比gomock轻量,不需要依赖接口

缺陷:

  • 对项目源代码有侵入性,即被打桩方法必须赋值给一个变量,只有以这种形式定义的方法才能别打桩

详情参见:GoStub框架使用指南

3.4 gomock

github地址:https://github.com/golang/mock

特性:

  • golang官方开发维护的接口级别的mock方案
  • 包含了GoMock包和mockgen工具两部分,其中GoMock包完成对桩对象生命周期的管理,mockgen工具用来生成interface对应的Mock类源文件。

缺陷:

  • 只有以接口定义的方法才能mock
  • 需要用mockgen生成源文件,然后用gomock去实现自己想要的数据,用法稍重。

详情参见:使用Golang的官方mock工具—gomock

https://www.jianshu.com/p/598a11bbdafb

和 GoMock框架使用指南

https://www.jianshu.com/p/f4e773a1b11f

3.5 gomonkey

github地址:https://github.com/bouk/monkey

特性:

  • 可以为全局变量、函数、过程、方法打桩,同时避免了gostub对代码的侵入

缺陷:

  • 对inline函数打桩无效
  • 不支持多次调用桩函数(方法)而呈现不同行为的复杂情况

详情参见:Monkey框架使用指南

https://www.jianshu.com/p/2f675d5e334e

3.6 sqlmock

github地址: https://github.com/DATA-DOG/go-sqlmock

特性:

  • 适用于和数据库的交互场景。可以创建模拟连接,编写原生sql 语句,编写返回值或者错误信息并判断执行结果和预设的返回值
  • 提供了完整的事务的执行测试框架,支持prepare参数化提交和执行的Mock方案
  • 持久层框架底层一般都使用”github.com/go-sql-driver/mysql”,所以一般都能够使用sqlmock库进行mock

缺陷:

  • 因为是正则匹配,所以可能漏掉sql的语法错误
  • 写入后没法验证

3.7 httpexpect

github地址:https://github.com/gavv/httpexpect

特性:

  • 适用于对http的clent进行测试,对服务端的回包进行打桩
  • 支持对不同方法(get,post,head等)的构造,支持自定义返回值json

sqlmock和httpexpect都蛮简单的,看完github主页的QuickStart基本就会用了~~

4 使用goconvey+gomonkey+sqlmock进行测试

4.1 选择原因

  • 外层框架——goconvey。项目代码很多逻辑比较复杂,需要编写不同情况下的测试用例,用goconvey组织的测试代码逻辑层次比较清晰,有着较好的可读性和可维护性。断言方面感觉convey和testify功能差不多。不过convey没有testify社区活跃度高,后续使用convey时碰到一些问题,都不太容易找到解决办法,给作者提issue,感觉回复效率也不是很高。
  • 函数mock——gomonkey。项目代码基本都不是基于interface实现的,所以不太方便使用gomock,项目目前运行稳定,所以也不想因为单元测试重构原来的代码,所以也不太方便gostub。好在还有gomonkey可以用,基本符合我们对函数打桩的需求。
  • 持久层mock——sqlmock。我们持久层的框架是gorm。当时考虑2种方法进行mock,一种是使用gomonkey对gorm的函数进行mock,另一种则是选用sqlmock。但碰到下图所示的sql语句,如果使用gomonkey的话需要对连续调用的gorm函数都进行mock,过于繁杂。而用sqlmock的话只需匹配对应的sql语句即可。
newDB = MysqlDB.ModelTable(c, &Basexxx{}, c.AppID()).Where("type = ?", libType).Limit(limit).Offset(offset).Order("created_at desc").Find(&libxxxs)

4.2 gorm+sqlmock使用方法

初始化sqlmock后,然后使用dialect和dsn打开一个新的gorm连接并赋值给数据库操作实例

 _, mock, _ = sqlmock.NewWithDSN("sqlmock_db")
 MysqlDB.DB, _ = gorm.Open("sqlmock", "sqlmock_db")

接下来就和sqlmock的普通使用没什么区别了,只要mock时能够成功的匹配gorm生成的sql语句即可

详情参见:Stub database connection with GORM

https://blog.valletta.io/blog/2018-07-05-stub-database-connection-with-gorm/

4.3 踩坑记录(持续更新~)

  1. 问题描述:
  2. 测试函数在run的时候fail,在无断点debug的时候pass。被patch的函数是下例中的函数A。
func A(arg string) error {
 return B(arg)
}

原因:

  • run的时候会做编译器优化,调用A会直接被优化为调用B(内联)。所以对前者的patch并没有成功。

在不改动原有代码的情况下,有2种解决方案:

  • 给函数B也打补丁
  • 在go test时加参数来避免编译器优化内联 go test -gcflags=-l

5 其他

5.1 单元测试的粒度

对于刚开始做单元测试的同学来说,如何把握单元测试的粒度是一个让人头疼的问题。

测试粒度做的太细,会耗费大量的开发以及维护时间,每改一个方法,都要改动其对应的测试方法。当发生代码重构的时候那简直就是噩梦(因为所有的单元测试又都要写一遍了…)。

如果单元测试粒度太粗,一个测试方法测试了n多方法,那么单元测试将显的非常臃肿,脱离了单元测试的本意,容易把单元测试写成集成测试。

5.2 单元测试的成本和收益

在受益于单元测试的好处的同时,也必然增加了代码量以及维护成本。

下面这张成本/价值象限图清晰阐述了在不同性质的系统中单元测试的成本和价值之间的关系。

  1. 依赖很少的简单代码(左下)
  2. 对于外部依赖少,代码又简单的代码。自然其成本和价值都是比较低的。
  3. 例如Go官方库里errors包,整个包就两个方法 New()和 Error(),没有任何外部依赖,代码也很简单,所以其单元测试起来也是相当方便。
  4. 依赖较多的简单代码(右下)
  5. 依赖一多,mock和stub就必然增多,单元测试的成本也就随之增加。但代码又如此简单,这个时候写单元测试的成本已经大于其价值,还不如不写单元测试。
  6. 依赖很少的复杂代码 (左上)
  7. 像这一类代码,是最有价值写单元测试的。比如一些独立的复杂算法(银行利息计算,保险费率计算,TCP协议解析等),像这一类代码外部依赖很少,但却很容易出错,如果没有单元测试,几乎不能保证代码质量。
  8. 依赖很多的复杂代码(右上)
  9. 这种代码显然是单元测试的噩梦。写单元测试吧,代价高昂;不写单元测试吧,风险太高。
  10. 像这种代码我们尽量在设计上将其分为两部分:1.处理复杂的逻辑部分 2.处理依赖部分 然后1部分进行单元测试。

参考链接

下面是一些其他的参考资料:

1. The-Golang-Standard-Library-by-Example

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/

2. 搞定Go单元测试(一)——基础原理

https://juejin.im/post/5ce93447e51d45775746b8b0#heading-12

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码