如何做好单元测试?Golang Mock”三剑客“ gomock、monkey、sqlmock

编程入门 行业动态 更新时间:2024-10-27 23:19:41

如何做好单元测试?Golang Mock”<a href=https://www.elefans.com/category/jswz/34/1746066.html style=三剑客“ gomock、monkey、sqlmock"/>

如何做好单元测试?Golang Mock”三剑客“ gomock、monkey、sqlmock

一、前言

单元测试一直是一个研发过程中老生常谈的话题,能够把单元测试做的比较好的公司也寥寥可数。最近同事开玩笑说最不喜欢的两件事情”接手的代码没有单测和别人让我写单测“,也能看得出大家对单测是又爱又恨。但真实情况是单测确实能够提高质量,一般公司架构团队或TL会要求业务研发有单测指标,但很容易因为 ”成本“ 问题最终以失败收尾,那怎么能够降低单测成本又能享受到单测带来和好处就是本文的”目的“了。

想要实现一个低成本的单测基本要从以下问题入手:

  • 代码可测性
  • 低成本mock
  • 逻辑断言工具

资料汇总:

  • 引用:
  • monkey 原理解读
    • .html

二、【新手入门】单元测试解决什么问题?

单元测试(unit test)是最小、最简单的软件测试形式、这些测试用来评估某一个独立的软件单元,比如一个类,或者一个函数的正确性。这些测试不考虑包含该软件单元的整体系统的正确定。单元测试同时也是一种规范,用来保证某个函数或者模块完全符合系统对其的行为要求。单元测试经常被用来引入测试驱动开发的概念。

在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数:

测试单数函数名前缀为Test测试程序的一些逻辑行为是否正确
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example提供示例

大家可以看到下面这张图,从最上面依次我们可以理解为“黑盒端到端测试”、“单服务接口测试”以及“方法级别的单元测试”,他们三者的会有两个维度的不同那就是成本和频率,黑盒UI测试一次迭代基本上完整的回归也就1~2次,每次测试按天计数,服务测试可以理解为借口自动化测试脚本,每次测试按环境发布次数计数,频率最高运行成本最低的就是单测,每次提交代码都可以运行一次单测检测。

PS: 单测还有一大成本就是本文提到的写单测的成本,如果这个成本和研发接口的成本差不多这就是一个糟糕的单测,如果单测只有一个接口研发的20%的成本那是非常值得做的。

三、你编码的时候考虑单测可测性了吗?

首先我们可以看看一些主流项目是如何写单测的:

  • .go
  • .go
  • .go
	tests := []struct {name    stringmessage stringwant    []string}{{name:    "no match",message: "Hello world!",want:    nil,},{name:    "contains issue numbers",message: "#123 is fixed, and #456 is WIP",want:    []string{"#123", " #456"},},{name:    "contains full issue references",message: "#123 is fixed, and user/repo#456 is WIP",want:    []string{"#123", " user/repo#456"},},}for _, test := range tests {t.Run(test.name, func(t *testing.T) {got := issueReferencePattern.FindAllString(test.message, -1)assert.Equal(t, test.want, got)})}

看完之后大家是不是都发现了同一个特点,阶段明准备数据->并发执行->断言结果,大家在看看自己开发的业务代码是否可以按照这种方式进行 “高比例覆盖” 呢?

如果可以证明你的可测性做的很不错,如果不行大家就要思考思考以下几个问题了:

  • func 的职责是否清晰,是否一个func只做一件事,是否足够简单
  • 是否有偷懒的入参出参,比较典型的就是一个超大的status向下传递,已经大大超出了这个方法原本需要的入参范围了
  • 一个方法代码量是否在一屏以内,一个200行的代码想要单测是否非常困难的,每一个逻辑嵌套都会带来单测成倍的工作量

四、选择合适mock工具事半功倍

对于这类开源项目或开源组件一般不会有mock的烦恼,因为它们依赖的中间件非常有限,但是对于我们业务开发就不一样了,每一个中间件都是强依赖,比较典型的就是数据库、cache、MQ了,单测时我们又没有真正意义上的中间件环境,那在读取数据返回结果时要怎么办呢?

那就要请我们三大武林高手:gomock、monkey、sqlmock出山了:

  • gomock:强依赖interface进行打桩
  • monkey:方法替换改写
    • (作者不在更新)
    • (已经支持arm和全部go版本)
    • 注意:monkey不支持内联函数,在测试的时候需要通过命令行参数 -gcflags=-l 关闭Go语言的内联优化。
    • monkey不是线程安全的,所以不要把它用到并发的单元测试中。
      • 解决方案:
  • sqlmock:通过中间件底层链接进行mock
    • github/DATA-DOG/go-sqlmock
    • github/go-redis/redismock
    • github/alicebob/miniredis/v2

mock三种主流方案对比:

评估项gomockmonkeysqlmock
代码侵入性强依赖interface,影响编码规范无侵入需要支持动态替换中间件实例
成本一般
灵活性每次增加方法都需要修改mock实现按需mock工具受限、场景受限

在看例子之前一句话概括这三种不同mock工具适用的场景:

  • gomock:适合于 业务代码分层互相依赖已经使用 interface 情况,或对第三方依赖是 interface 情况下使用
  • monkey:万金油无论是对方法、变量都可以mock,甚至官方函数都行,但不支持并行测试,改写方法是全局生效的
  • sqlmock:不太适合func的单测会增加单测范围以及反调用直觉,比较适合于一一个服务的全流程单测

gomock

func Test_GetCountriesList_ToGoMock(t *testing.T) {Convey("Countries_ToGoMock", t, func() {ctlCity := gomock.NewController(t)defer ctlCity.Finish()ctlCountries := gomock.NewController(t)defer ctlCountries.Finish()cityToolMock := mock_Model.NewMockCityTool(ctlCity)countriesMock := mock_Model.NewMockCountriesTool(ctlCountries)c := []model.Countries{{Id:            "CN",Native:        "中国",CallingCode:   86,OfficialId:    "CHN",Region:        "Asia",CountriesIcon: ".svg",Zh:            "中国",En:            "China",},}gomock.InOrder(countriesMock.EXPECT().GetMapListByType().Return(c, nil),)mapTool := NewMap(cityToolMock, countriesMock)rs, _ := mapTool.GetCountriesList()So(rs[0].Zh, ShouldEqual, "中国")})
}

monkey

func Test_GetCountriesList_ToMonker(t *testing.T) {Convey("err", t, func() {p := monkey.PatchInstanceMethod(reflect.TypeOf(&model.Countries{}), "GetMapListByType", func(_ *model.Countries) ([]model.Countries, error) {c := []model.Countries{{Zh: "中国",},}return c, nil})defer p.Unpatch()rs, _ := MapHandelr.GetCountriesList()So(rs[0].Zh, ShouldEqual, "中国")})}

sqlmock

func Test_GetCountriesList_SqlMock(t *testing.T) {Convey("error", t, func() {//把匹配器设置成相等匹配器,不设置默认使用正则匹配db, mock, err := sqlmock.New()if err != nil {panic(err)}rows := sqlmock.NewRows([]string{"zh"}).AddRow("中国")mock.ExpectQuery("^SELECT \\* FROM `countries`").WillReturnRows(rows)_DB, err := gorm.Open("mysql", db)model.MockDB = _DBrs, _ := MapHandelr.GetCountriesList()fmt.Println(rs)So(rs[0].Zh, ShouldEqual, "中国")})
}

五、单测工具推荐

断言是单测的灵魂,市面上大多数工具都主要提供的是更好的断言能力。

主流断言工具 github/stretchr/testify

testify 绝大多数github开源软件都在使用testify

go get github/stretchr/testify/assertfunc TestSomething(t *testing.T) {assert := assert.New(t)// assert equalityassert.Equal(123, 123, "they should be equal")// assert inequalityassert.NotEqual(123, 456, "they should not be equal")// assert for nil (good for errors)assert.Nil(object)// assert for not nil (good when you expect something)if assert.NotNil(object) {// now we know that object isn't nil, we are safe to make// further assertions without causing any errorsassert.Equal("Something", object.Value)}
}

【强烈推荐】流程化单测工具:github/smartystreets/goconvey

文档:

了解单测的小伙伴一定听说过 ”表格驱动测试“,先定义一堆输入,然后循环测试方法,这里介绍到的goconvey可以称作 ”逻辑驱动测试“,编写单测可以和业务逻辑结合使用goconvey编写一颗逻辑树来覆盖不同的代码分支逻辑。

并且goconvey也有丰富的So断言也支持自定义断言:

package package_nameimport ("testing". "github/smartystreets/goconvey/convey"
)func TestSpec(t *testing.T) {// Only pass t into top-level Convey callsConvey("Given some integer with a starting value", t, func() {x := 1Convey("When the integer is incremented", func() {x++Convey("The value should be greater by one", func() {So(x, ShouldEqual, 2)})})})
}

goconvey自带命令行和可视化工具,项目下执行 “goconvey” 命令回自动打开页面并执行单测

点击目录查看具体代码覆盖率情况:

其他各种包:

  • github/frankban/quicktest
  • gotest.tools/v3/assert
  • go.uber/zap/zaptest

更多推荐

如何做好单元测试?Golang Mock”三剑客“ gomock、monkey、sqlmock

本文发布于:2024-02-27 07:39:49,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1705739.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:三剑客   如何做好   单元测试   Golang   Mock

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!