2018-04-28 21:24:17

关于 Go 的集成测试

好久没写博客了,不是我懒,是工作确实逐渐忙起来了。近来稍微闲了一些,又陆续学一些新东西,因此会逐渐总结记录一些新的博文

前言

第一次系统地接触测试是大三的软件测试和软件测试课程设计两门课,前者学理论后者写代码,虽然一直吐槽学校里啥都没学到,但这门课真的是少有的几门确实有用的课了。

比较有意思的是当时教材是老师自己写的,但是没人买导致出版社都不印刷,老师让我们自己去复印,不过确实这本书相当有水平。期末课程是找出以前的一个期末大项目为它写测试,单元测试、集成测试、系统测试、性能测试、白盒黑盒,并且必须做到用例输入输出的 excel 自动化以及撰写对应的用例文档和测试结果,当时由于老师定死了只能选 c++ 和 java 写的项目,因为 java 被坑爹队友坑的原因我们作死选了一个安卓 app。最后答辩前两天才搞定测试代码,个中滋味难以言表,搞到连我这个只会一点后端的都知道 AndroidTest 和 Test 的区别了。

写测试给我带来的帮助非常大,现在的我认为,能有机会写测试甚至达到一定覆盖率是一件非常幸福的事,特别是对于那些规模大到需要考虑脱敏、专门起额外的测试用docker甚至直接持久化、生产实际场景多到测试用例难以覆盖、系统本身由多语言共同支撑、连接池的实现方式都需要纳入测试范围的项目来说。不需要验证生产是否正常,只要测试通过就能认为功能完成的这种项目,我相信是任何开发者都想接手的

go 的集成测试

go 是目前我接触到的唯一一个自带测试框架的语言,并且社区接受度还相当高,基本一个包对应都会有一个简单小巧的测试文件,所以 go 项目没测试会是一件让人非常遗憾的事情。

集成测试需要考虑模块间的依赖,其中肯定绕不过启动依赖和模块导出,以及不看文档时很容易漏掉的 TestMain

去除 init 函数

将模块中的 init 函数用导出函数替代,并在集成测试的测试入口统一初始化。否则集成测试的测试环境启动很难保证模块启动的一致性,你真的能理清楚包导入和包 init 的顺序么?

模块导出函数的技巧

package test1
...
// CallFunc is the exported func of this package
func CallFunc(){
  RealFunc()
}

// Only for test
func RealFunc() {

}

考虑如上的代码,看起来调用 CallFunc 的话绕了一层才调用真正有用的 RealFunc,貌似多此一举,但是,在集成测试代码中可以非常容易的将 RealFunc 给 mock 掉:

import (
  "..../test1"
)

func init() {
  test1.RealFunc = func () {
    log.Println("gotta")
  }
}

这就达到了和我之前写的一篇 nodejs 集成测试 mock 中的一样的效果。对于集成测试来说,导出一些常用常量也是经常的事情,即使对于单个模块来说导出它们看起来并没有意义。

装饰器

对于大系统来说,上段的 mock 办法会导致代码比较难看,没注释的情况下简直是灾难,此时可以有意识地考虑装饰器。

go 没有内置的装饰器,因此只能使用函数作为参数来创建一个新的函数这种办法手动模拟,这时导出装饰器函数即可,无论是语义还是结构都会比单纯地导出实际函数要好

另外集成测试 mock 函数中 defer 很有用处

build tag

如果需要 mock 的函数较少,如上的导出办法还可以,一旦多起来就不好办了。最初我准备使用公司内置的 ci template 把代码分成测试和生产两份,后来发现 go 的构建变量自己做了这件事情,只需要在文件顶上增加一句// +build debug或者// +build !debug注释,即可在构建时通过运行go build -tags debug 将正常的文件构建进去。

TestMain

TestMain 可以很方便地处理测试开始和结束的情况,特别适合测试用例并发执行后需要等待并处理 channel 消息的情况,例如如下代码:

func init() {
    runtime.LockOSThread()
}

func TestMain(m *testing.Main) {
    go func() {
        os.Exit(m.Run())
    }()
    lockFunc()
}

这里 lockFunc 是一个阻塞函数。说到阻塞进程的函数,对 go 这种有协程的语言倒不觉得奇怪,nodejs 中 egg 框架处理 websocket 的函数也是阻塞的,看起来就很诡异了,类似下面的 koa 代码:

router.get('/', (ctx, next) => {
  // logic for websocket
  return next()
}, ctx => ctx.body = 'never reach here')

导出辅助测试的代码

除了集成测试外,还建议提供对外的测试辅助代码,诸如 NewMockServer之类的能够一键模拟本模块功能的东西。有这种东西,用你的包的开发者会很爽的。由于源码编译的特性,这种情况在 go 生态里也不算罕见

最后

当初软件测试那学期有另一门课,老师为了验收项目专门花钱买了一个嵌入式设备,结果最后因为只有安卓 app 又解析不了它的 WIFI 协议进而连不进去测试不了而草草收尾。这位老师不久后移民去了新西兰,现在想起来也挺有趣的

本文链接:https://smallpath.me/post/something-about-go-intergration-test

-- EOF --