GO语言学习笔记-测试篇 Study for Go ! Chapter ten- Test

持续更新 Go 语言学习进度中 ......

  1. GO语言学习笔记-类型篇 Study for Go! Chapter one - Type - slowlydance2me - 博客园 (cnblogs.com)
  2. GO语言学习笔记-表达式篇 Study for Go ! Chapter two - Expression - slowlydance2me - 博客园 (cnblogs.com)
  3. GO语言学习笔记-函数篇 Study for Go ! Chapter three - Function - slowlydance2me - 博客园 (cnblogs.com)

Study for Go ! Chapter ten- Test

1. 单元测试 ( unit test )

单元测试 ( unit test )除用来测试逻辑算法是否符合预期外,还承担着监控代码质量的责任。任何时候都可以用简单的命令来验证全部功能,找出未完成任务 ( 验收 ) 和任何因修改而造成的错误。它与性能测试、代码覆盖率等一起保证了代码总是在可控范围内,这远比形式化的人工检查要有用得多

单元测试并非要取代人工代码审查 ( code review ),实际上它也无法切入到代码实现层面。但可通过测试结果为审查提供筛选数据,避免因繁琐导致代码审查沦为形式主义。单元测试可自动化进行,能持之以恒。但测试毕竟只是手段,而非目的,所以如何合理安排测试就需要开发人员因地制宜

可以将测试、版本管理工具、以及自动发布 (nightly build)整合。编写脚本将测试失败结果与代码提交日志相匹配,最终生成报告发往指定邮箱

很多人认为单元测试代码不好写,不知道怎么测试。如果非技术原因,那么需要考虑结构设计是否合理,因为可测试性也是代码质量的一个体现

写单元测试本身就是对即将要实现的算法做复核预演。因为无论什么算法都需要给如条件,返回预期结果。这些加上平时写在 main 里面的临时代码,本就是一个完整的单元测试用例,无非换个地方存放而已

Testing

  • 工具链和标准库自带单元测试框架,这让测试工作变得相对容易。关于测试,有以下规则:

    • 测试代码须放在当前包以“ _test.go ” 结尾的文件中

    • 测试函数以 Test 为名称前缀

    • 测试命令 ( go test )忽略以 “ _ ” 或 “ . ” 开头的测试文件

    • 正常编译操作 ( go build/install )会忽略测试文件

  • 标准库 testing 提供了专用类型 T 来控制测试结果和行为

  • 使用 Parallel 可有效利用多核并行优势,缩短测试时间

对于测试是否应该和目标放在同一目录,一直有不同的看法。某些人认为应该另建一个专门的包用来存放单元测试,且只测试目标公开接口。好处是,当目标内部发生变化时,无须同步维护测试代码。每个人对于测试都有不同的理解,就像覆盖率是否要做到 90% 以上,也是见仁见智

Table driven

  • 单元测试代码一样要写的简介优雅,要做到这一点并不容易。好在多数时候,我们可以用一种类似数据表的模式来批量输入条件并依次比对结果

  • 这种方式将测试数据和测试逻辑分离,更便于维护。另外,使用 Error 是为了让整个表全部完成测试,以便知道具体是哪组条件出了问题。

Test main

  • 某些时候,须为测试用例提供初始化和清理操作,但 testing 并没有 setup / teardown 机制。解决办法是自定义一个名为 TestMain 的函数,go test 会改为执行该函数,而不再是具体的测试用例

  • M.Run 会调用具体的测试用例,但麻烦的是不能为每一个测试文件写一个 TestMain

  • 要实现用例组合套件 ( suite ),需借助 MainStart 自行构建 M 对象。通过与命令行参数相配合,即可实现不同测试组合

Example

  • 例代码最大的用途不是测试,而是导入到 GoDoc 等工具生成的帮助文档中。它通过比对输出 ( stdout )结果和内部 output 注释是否一致来判断是否成功

  • 如果没有 output 注释,函数就不会被执行。另外,不能使用内置函数 print/println 因为它们输出到 stderr

2. 性能测试

  • 性能测试函数以 Benchmark 为名称前缀,同样保存在 “ *_test.go ” 文件里

  • 测试工具默认不会执行性能测试,须使用 bench 参数。它通过逐步调整 B.N 值反复执行测试函数,知道获得准确的测量结果

  • 如果仅希望进行性能测试,可以用 run = NONE 忽略所有单元测试用例

  • 某些耗时的目标,默认循环次数过少,取平均值不足以准确计量性能。可使用 benchtime 设定最小测试时间来增加循环次数,以便返回更准确的结果

Timer

  • 如果在测试函数中要执行一些额外操作,那么应该临时阻止计时器工作

Memory

  • 性能测试关心的不仅仅是执行时间,还包括在堆上的内存分配,因为内存分配和垃圾回收的相关操作也应该计入消耗成本

  • 也可以将测试函数设置为总是输出内存分配信息,无论使用 benchmem 参数与否

3. 代码覆盖率

  • 如果说单元测试和性能测试关注代码质量,那么代码覆盖率 ( code coverage)就是度量测试自身完整和有效性的一种手段

  • 通过覆盖率值,我们可以分析出测试代码的编写质量。检测它是否提供了足够的测试条例,是否执行了足够的函数、语句、分支、和代码行等,依次来量化测试本身,让白盒测试真正起到应有的质量保障作用。

  • 当然,这并不是说要追求形式上的数字百分比。关键还是为改进测试提供一个可发现缺陷的机会,毕竟只有测试本身的质量得到保障,才能让它免于成为形式主义摆设

  • 代码覆盖率也常用来发现死代码 ( dead code )

  • 为获取更详细的信息,可指定 covermode 和 coverprofile 参数

    • set :是否执行

    • count: 执行次数

    • atomic:执行次数 (支持并发模式)

    • 还可以在浏览器中查看包括具体的执行次数等信息

4. 性能监控

  • 引发性能问题的原因无外乎执行时间过长,内存占用过多,以及意外阻塞。通过捕获或监控相关执行状态数据,就可定位引发问题的原因,从而有针对性改进算法

  • 有两种捕获方式:首先,在测试时输出并保存相关数据,进行初级评估。其次,在运行阶段通过 Web 接口获得实时数据,分析一段时间内的健康情况。除此之外,我们还可以用自定义计数器 ( expvar )提供更多与逻辑相关的参考数据