吾尝终日而思矣,不如须臾之所学也;吾尝跂而望矣,不如登高之博见也。登高而招,臂非加长也,而见者远;顺风而呼,声非加疾也,而闻者彰。假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也。

-- 荀况 《劝学》


Go 语言对于单元测试是很重视的,且不说其他的作者的背景啊,开源库啊,第三方的支持之类的,有两点让我对 Go 语言关于单元测试的重视程度的有信心的点在于:

  1. Go 语言源代码和内置库自身的单元测试完备性
  2. Go 语言自带单元测试命令

从这两点,我认为测试在 Go 语言中具有非常重要的地位,所以在这篇文章中,我也尝试讲一些关于 Go 语言单元测试的东西。

编写 Go 单元测试代码

Go 的测试方法看上去相对比较低级,它依赖于命令 go test 和一些能用 go test 运行的测试函数的编写约定。但是,我认为这就是所谓的 Go 风格,用 Go 以来,我的感受是 Go 语言就是保持了 C 语言编程习惯的一门语言。

首先,为了开始这篇文章,我写写一个简单的函数用作后面要测试的例子,但是,考虑到后面可能要讲一些稍微复杂一点的内容,所以,这个例子我留有一些可以改变的地方,大家可以选择着看:

这个例子就是这么简单,将这个文件命名为 main.go,然后我们就应该编写测试代码了。测试代码的文件放置的位置可以随意,package 也可以随意写,但是,文件名必须以 _test 结尾,所以,我这里就命名为 main_test.go

这里编写测试函数,有几个需要注意的点:

  1. 每个测试文件必须以 _test.go 结尾,不然 go test 不能发现测试文件
  2. 每个测试文件必须导入 testing
  3. 功能测试函数必须以 Test 开头,然后一般接测试函数的名字,这个不强求

根据这些条件,我们可以写出一个测试文件:

测试文件写完之后,我们就应该执行测试了,打开命令行工具,敲入这条命令:go test main_test.go main.go -v -cover

然后就应该等待测试结果了,这里加了两个参数,分别是 -v-cover,如果不加上的话你会发现只有 Test Pass 的简单提示,而看不到我们加了参数的具体提示:

=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
coverage: 50.0% of statements
ok command-line-arguments 0.008s

基于表的测试方式

在 Go 语言中,有一种常用的测试套路,叫做基于表的测试方式,其核心就是我们需要针对不同的场景,其实也就是不同的输入和输出来验证一个功能。例如我们要验证的 Add 函数,我们需要验证的功能点有很多,例如:

  • 两个正数相加是否正确
  • 两个负数相加是否正确
  • 一个正数加上一个负数是否正确
  • 有一个数为 0 是否正确

那么,我们就可以使用 基于表的测试方式 了,代码可以这样写:

Mock 依赖

前面介绍的测试都是比较简单的,功能简单的话我们就可以直接给定输入,然后看输出是否符合预期,这样就可以很简单得写完单元测试了。但是,有的时候,由于业务逻辑的复杂性,功能代码并不会就这么直接,往往还会掺杂很多其他组件,这就给我们的测试工作带来很大的麻烦,我这里列举几个常见的依赖:

  • 组件依赖
  • 函数依赖

组件依赖和函数依赖是两种比较常见的依赖,但是,这两种依赖也是可以扩展开来说的,既可能来自于我们自己编写的组件/函数,也可能是引入其他人写的。但是,无妨,对于这些情况,我们都会做一些分析。

组件依赖处理

使用 Go 语言开发项目的时候,我们应该经常会抽离组件模块的,既然抽离了组件和模块,那么久离不开组件的依赖了,既然有依赖,在测试的时候我们很多时候都是希望屏蔽掉依赖组件的影响,从而更好得测试现有代码的细节;或者说,我们希望根据自己的测试目标,控制依赖组件的行为。但是,如果我们还想简单得通过控制输入和输出来控制依赖组件的行为,这个难度还是比较大的,所以,在这种情况下,我们一般会考虑传一个 Stub 组件进入,从而达到控制依赖组件行为的效果。

举一个例子先,例如我们比较常见的 Service 层和 DAO 层的操作,Service 处理完逻辑之后,交给 DAO 层进行持久化,或者需要调用 DAO 层从持久化中获取一些必要的数据;在测试的时候,我们很多时候不希望真的持久化或者从持久化中获取数据,那么就会对 DAO 层进行一些 Mock。首先,先给出一个运行的例子:

这里我们想要测试 Service 的正确性,但是又不想要真的持久化 DAO,所以,这个时候我们会自己创建一个 Stub,然后提供给 Service,同时,我们还能操作 DAO 的行为,达到运行得效果,例如:

这样我们就能测试到登录成功的代码了,登录成功的测试了,我们还希望测试一下登录失败的,那么也很简单,修改一下就可以了呗:

这里对测试代码稍微改了一下,可以发现,我们可以通过修改一个变量来控制 Stub 的输出,从而达到测试不同功能的效果,这就解决了组件依赖的问题。

函数依赖

函数依赖相比于组件依赖会更麻烦一点,因为我们在前面可以看到,组件依赖的话我们可以传递 Stub 进行,这样我们可以随意得控制 Stub 的行为,但是函数不行呀,这里我们又不能传函数进去,因为函数是被 import 进去的啊。问题就在这了,因为函数是被 import 进去的,所以可以理解为函数是全局的了,既然这样,那么我们为什么不修改一下函数呢?什么意思?我们先来看着正常的业务例子:

这里我是想表达的一个意思就是需要先登录,然后登录完之后我们才能回复消息,这里我们的登录逻辑是简单的,但是,在实际业务中可能这里的登录逻辑就设计到 DB 访问等等,我们希望不走真实的逻辑,而是自己来控制 Login 的行为。

首先,先分析一下我们的 UT 目的,我们的目的是测试 Reply 函数,我们期望是 Login 成功,那么 Reply 也应该是成功的;如果 Login 失败,那么 Reply 也应该是失败的。这个测试结论不应该被 Login 所影响,及时以后 Login 逻辑修改了,我们也应该是这个逻辑,不会受到影响,那么我们可以这么编写 UT:

这里可以发现,我们是修改了 Login 这个函数的代码,从而控制 Login 函数的返回值,这样我们就可以测试我们写的代码的逻辑是否正确了。

可能有心细的同学发现,第二个函数依赖里面的代码有个特别的地方,在于函数 Login 是个变量,为什么要用函数变量呢,不能直接使用函数吗?这里确实是一个麻烦得地方,因为如果使用直接函数的话,我们没得赋值,那么也就无法修改它了。如果代码不是我们自己写的,而是使用的其他同事的代码,那么问题就大了。这种情况下,我们可以怎么处理呢?

在 Reference 的中有一篇文章:Mocking functions in Go 介绍了一种方法,那就是将我们引用的函数赋值为函数变量再使用,从而达到同样的效果。

第三方库

在前面的介绍中,我们都是自己重新写了一个 Stub 类,但是同时问题暴露了,写起来比较复杂和繁琐,所以就有了一些第三方库可以方便我们编写这些东西,Gomock 就是一个,这里就简单演示一下 GoMock 的一些功能。

gomock 有两个组件,分别是 gomockmockgenmockgen 可以根据我们的 interface 生成对应的 Stub 对象,例如我们前面提到的 DAO,因为我们 DAO 的 Interface 已经写好了,所以我们可以很方便得生成 StubDao,只需要使用命令:

$ mockgen -source=main.go > mock_dao.go

然后我们查看一下 mock_dao.go 的内容:

可以发现两个函数都被实现了 ReadAllSaveData,但是,同时我们也发现,生成的 DAO 比我们预期的要复杂得多,至于为什么药这么复杂,看看接下来的使用就明白了。现在我们已经有了 DAO,那么下一步就是控制 DAO 的输出了。

这里可以看到,使用 gomock 可以让我们的控制输出更加简便,之前我们需要通过控制变量的方式来达到控制输出的目的,但是这里可以很方便得使用:

d.EXPECT().FuncXXX.Return(xxxx)

来指定函数应该返回什么结果,确实方便了,同时,还省去了我们自己编写 MockClass 的时间,这个还是值得一试的。

总结

OK,本文关于 Go 的 UT 就差不多这么多了,这里介绍了 Go 单元测试的编写套路,以及介绍了我们在项目中常见的一些场景的处理,最后再介绍了一款第三库用于加快 UT 的编写。在我另一篇文章:从开源项目看 Python 单元测试 中我曾经提到,UT 是我们都希望别人写,但是自己又不想写的东西,希望,Go 语言相对简单的编写套路和模式能够让大家对 UT 有所重视,并且愿意写起来。lol。

Reference

  1. Go程序设计语言
  2. Mocking functions in Go

Go 语言编写单元测试的更多相关文章

  1. C语言之单元测试

    在ITOO高校云平台项目实践中,我们模板的模块因为在调别人的接口时出现了问题,为了弄明白是不是接口出了问题,就必须学会单元测试. WHAT? 单元测试(unit testing),是指对软件中的最小可 ...

  2. Google C++单元测试框架GoogleTest---GTest的Sample1和编写单元测试的步骤

    如果你还没有搭建gtest框架,可以参考我之前的博客:http://www.cnblogs.com/jycboy/p/6001153.html.. 1.The first sample: sample ...

  3. 基于php基础语言编写的小程序之计算器

    基于php基础语言编写的小程序之计算器 需求:在输入框中输入数字进行加.减.乘.除运算(html+php) 思路: 1首先要创建输入数字和运算符的输入框,数字用input的text属性,运算符用sel ...

  4. 如何用C语言编写病毒‘

    怎样用C语言编写病毒在分析病毒机理的基础上,用C语言写了一个小病毒作为实例,用TURBOC2.0实现.[Abstract] This paper introduce the charateristic ...

  5. Intellij Idea系列之导Jar包与编写单元测试(二)

     Intellij Idea系列之导Jar包与编写单元测试(二) 一.初衷 对于很多的初学者来说,Intellij如何导入jar包感到很迷惑,甚至在网上搜过相关文章之后还是云里雾里,本博客通过图文并茂 ...

  6. 选择使用c语言编写的phalcon框架

    使用这个框架,我总结了如下几点考虑 1.这个框架速度快.纯c语言编写的框架,速度都比php框架快,省去了中间环节.当然,使用它不仅仅是性能考虑.因为如果为了解决php性能问题,完全可以有很多种方式,不 ...

  7. [改善Java代码]易变业务使用脚本语言编写

    建议16: 易变业务使用脚本语言编写 Java世界一直在遭受着异种语言的入侵,比如PHP.Ruby.Groovy.JavaScript等,这些“入侵者”都有一个共同特征:全是同一类语言—脚本语言,它们 ...

  8. 运用Python语言编写获取Linux基本系统信息(三):Python与数据库编程,把获取的信息存入数据库

    运用Python语言编写获取Linux基本系统信息(三):Python与数据库编程 有关前两篇的链接: 运用Python语言编写获取Linux基本系统信息(一):获得Linux版本.内核.当前时间 运 ...

  9. 运用Python语言编写获取Linux基本系统信息(二):文件系统使用情况获取

    本文跟着上一篇文章继续写,上一篇文章的链接 运用Python语言编写获取Linux基本系统信息(一):获得Linux版本.内核.当前时间 一.随便说说 获取文件系统使用情况的思路和上一篇获取主要系统是 ...

随机推荐

  1. 关于pipelineDB调用GetLocalStreamReaders的BUG

    如果想获取一个stream所有的reader,那么必须调用这个函数: Bitmapset *targets = GetLocalStreamReaders(relid); 如果stream下面没有re ...

  2. .NET版支付宝商户会员卡接入

    最近公司计划对接支付宝会员卡功能,而任务恰巧由领导安排给我这边,小弟之前也未做过支付宝接口,研究了三天,终于将支付宝会员卡API接口大体上调通了,现将其整理下,以供参考. 蚂蚁金服开发平台-商户会员卡 ...

  3. 简陋的斗地主,js实现

    最近闲了两天没事做,用js写了个斗地主,练习练习.代码和功能都很简陋,还有bug,咋只是聊聊自己的思路. 这里说说斗地主主要包含的功能:洗牌,发牌,玩家出牌.电脑出牌,出牌规则的验证,输赢啥的没有判断 ...

  4. JavaScript tips ——搞定闰年

    前言 处理时间时,常常要考虑用户的输入是否合法,其中一个很典型的场景就是平闰年的判断,网上其实有很多类似的算法,但是其实不必那么麻烦,下面我讲讲的我的思路. 规则 公元年数可被4整除为闰年,但是整百( ...

  5. 二叉树的递归遍历 Tree UVa548

    题意:给一棵点带权的二叉树的中序和后序遍历,找一个叶子使得他到根的路径上的权值的和最小,如果多解,那该叶子本身的权值应该最小 解题思路:1.用getline()输入整行字符,然后用stringstre ...

  6. ofBiz-groovy-freemarker

    ofBiz-groovy-freemarker根据浏览器的地址不同进入不同的页面 第一步:(2选一)创建groovy文件,或者java文件.在文件中定义变量 要放在 request.setAttrib ...

  7. Django模型中value函数运用

    values(*fields) 这个方法返回的是ValuesQuerySet,是QuerySet 的子类,也就是说,你可以用QuerySet里的方法. 需要注意的是,返回的不是list,不要直接当li ...

  8. opensslBIO系列之2---BIO结构和BIO相关文件介绍

    BIO结构和BIO相关文件介绍     (作者:DragonKing Mail:wzhah@263.net 公布于:http://gdwzh.126.com openssl专业论坛)          ...

  9. 【博客目录】SqlServer篇

    SqlServer系列篇   [SqlServer系列]SQLSERVER安装教程     [SqlServer系列]数据库三大范式     [SqlServer系列]表单查询     [SqlSer ...

  10. wget 下载百度网盘文件

    上传文件到服务器,有许多种方法,罗列一下我用过的 xftps之类的工具 rz tz命令 git 上传到码云 通过wget方式,上传文件到百度网盘,七牛云等只要支持wget方式下载即可 下面介绍一下怎么 ...