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

-- 荀况 《劝学》


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. Swift 线程安全数组

    欢迎大家前往腾讯云社区,获取更多腾讯海量技术实践干货哦~ 作者:BigNerdCoding 有并发的地方就存在线程安全问题,尤其是对于 Swift 这种还没有内置并发支持的语言来说线程安全问题更为突出 ...

  2. java web学习笔记 servlet

    关于java web web.xml中一般配置的都是与servlet先关的可以配置servlet filter listener context-param用来配置web应用的启动参数,可用通过Ser ...

  3. 42.Linux应用调试-初步制作系统调用(用户态->内核态)

    1首先来讲讲应用程序如何实现系统调用(用户态->内核态)? 我们以应用程序的write()函数为例: 1)首先用户态的write()函数会进入glibc库,里面会将write()转换为swi(S ...

  4. linux_Mysql导入数据基本操作

    创建数据库:Databases 数据库名字;导入数据:    mysql -uroot -proot use   数据库名字 source < sql文件名.sql

  5. 在java项目中加入百度富文本编辑器

    富文本编辑器在项目中很常见,他可以将文本,图片等信息存入数据库,在编辑一些图文混排的信息的时候很有用,比如商城项目的商品详情页,包含很多带有样式的文字和图片. 此前一直使用的百度的富文本编辑器uedi ...

  6. 项目实战12.2—企业级监控工具应用实战-zabbix操作进阶

    无监控,不运维.好了,废话不多说,下面都是干货. 流量党勿入,图片太多!!! 项目实战系列,总架构图 http://www.cnblogs.com/along21/p/8000812.html 一.U ...

  7. 快收藏!高手Linux运维管理必备工具大全,你会吗?

    一.统一账号管理 1.LDAP 统一管理各种平台帐号和密码,包括但不限于各种操作系统(Windows.Linux),Linux系统sudo集成,系统用户分组,主机登入限制等:可与Apache,HTTP ...

  8. attr设置checked,disabled等属性失效的问题,jquery的attr和prop的区别

    最近做项目遇到一个问题,radio设置了默认checked值,attr("checked",true)切换checked值失效 最后发现是jquery1.6版本之后,attr和pr ...

  9. oracle 主键自增 设置----杜恩德

    <div id="topicList"> <div class="forFlow"> <div class = "pos ...

  10. 5.python函数

    一.递归函数 如果一个函数在内部调用自身,那么这个函数就叫做递归函数. 1. 必须有一个明确的结束条件: 2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少: 3.递归效率不高,递归层次过 ...