《Go语言圣经》学习笔记 第十一章 测试
目录
- go test
- 测试函数
- 测试覆盖率
- 基准测试
- 剖析
- 示例函数
注:学习《Go语言圣经》笔记,PDF点击下载,建议看书。
Go语言小白学习笔记,书上的内容照搬,大佬看了勿喷,以后熟悉了会总结成自己的读书笔记。
- Maurice Wilkes, 第一个存储程序计算机EDSAC的设计者, 1949年他在实验室爬楼梯时有一个顿悟。 在《计算机先驱回忆录》 ( Memoirs of a Computer Pioneer) 里, 他回忆到: “忽然间有一种醍醐灌顶的感觉, 我整个后半生的美好时光都将在寻找程序BUG中度过了”。 肯定从那之后的大部分正常的码农都会同情Wilkes过份悲观的想法, 虽然也许不是没有人困惑于他对软件开发的难度的天真看法。
- 现在的程序已经远比Wilkes时代的更大也更复杂, 也有许多技术可以让软件的复杂性可得到
控制。 其中有两种技术在实践中证明是比较有效的。 第一种是代码在被正式部署前需要进行
代码评审。 第二种则是测试, 也就是本章的讨论主题。 - 我们说测试的时候一般是指自动化测试, 也就是写一些小的程序用来检测被测试代码( 产品
代码) 的行为和预期的一样, 这些通常都是精心设计的执行某些特定的功能或者是通过随机
性的输入要验证边界的处理。 - 软件测试是一个巨大的领域。 测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。 和软件测试技术相关的图书或博客文章有成千上万之多。 对于每一种主流的编程语言, 都会有一打的用于测试的软件包, 同时也有大量的测试相关的理论, 而且每种都吸引了大量技术先驱和追随者。 这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。
- Go语言的测试技术是相对低级的。 它依赖一个go test测试命令和一组按照约定方式编写的测试函数, 测试命令可以运行这些测试函数。 编写相对轻量级的纯测试代码是有效的, 而且它很容易延伸到基准测试和示例文档。
- 在实践中, 编写测试代码和编写程序本身并没有多大区别。 我们编写的每一个函数也是针对每个具体的任务。 我们必须小心处理边界条件, 思考合适的数据结构, 推断合适的输入应该产生什么样的结果输出。 编程测试代码和编写普通的Go代码过程是类似的; 它并不需要学习新的符号、 规则和工具。
1. go test
- go test命令是一个按照一定的约定和组织的测试代码的驱动程序。 在包目录内, 所有以_test.go为后缀名的源文件并不是go build构建包的一部分, 它们是go test测试的一部分。
- 在*_test.go文件中, 有三种类型的函数: 测试函数、 基准测试函数、 示例函数。 一个测试函数是以Test为函数名前缀的函数, 用于测试程序的一些逻辑行为是否正确; go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。 基准测试函数是以Benchmark为函数名前缀的函数, 它们用于衡量一些函数的性能; go test命令会多次运行基准函数以计算一个平均的执行时间。 示例函数是以Example为函数名前缀的函数, 提供一个由编译器保证正确性的示例文
档。 我们将在11.2节讨论测试函数的所有细节, 病在11.4节讨论基准测试函数的细节, 然后11.6节讨论示例函数的细节。 - go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数, 然后生成一个临时的main包用于调用相应的测试函数, 然后构建并运行、 报告测试结果, 最后清理测试中生成的临时文件
2. 测试函数
- 每个测试函数必须导入testing包。 测试函数有如下的签名:
- 测试函数的名字必须以Test开头, 可选的后缀名必须以大写字母开头:
- 其中t参数用于报告测试失败和附加的日志信息。 让我们定义一个实例包gopl.io/ch11/word1,其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。( 下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次; 我们稍后会再讨论这个问题。 )
- gopl.io/ch11/word1
- 在相同的目录下, word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。 每一个都是测试IsPalindrome是否给出正确的结果, 并使用t.Error报告失败信息:
- go test 命令如果没有参数指定包那么将默认采用当前目录对应的包( 和 go build 命令一样) 。 我们可以用下面的命令构建和运行测试。
- 结果还比较满意, 我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。 不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。 另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。 执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。
- 为了避免两次输入较长的字符串, 我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。
- 当添加了这两个测试用例之后, go test 返回了测试失败的信息
- 先编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。 只有这样, 我们才能定位我们要真正解决的问题。
- 先写测试用例的另外的好处是, 运行测试通常会比手工描述报告的处理更快, 这让我们可以进行快速地迭代。 如果测试集有很多运行缓慢的测试, 我们可以通过只选择运行某些特定的测试来加快测试速度。
- 参数 -v 可用于打印每个测试函数的名字和运行时间:
- 参数 -run 对应一个正则表达式, 只有测试函数名被它正确匹配的测试函数才会被 go test 测试命令运行:
- 当然, 一旦我们已经修复了失败的测试用例, 在我们提交代码更新之前, 我们应该以不带参数的 go test 命令运行全部的测试用例, 以确保修复失败测试的同时没有引入新的问题。
- 我们现在的任务就是修复这些错误。 简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列, 所以像“été”中的é等非ASCII字符不能正确处理。 第二个BUG是因为没有忽略空格和字母的大小写导致的。
- 针对上述两个BUG, 我们仔细重写了函数:
- gopl.io/ch11/word2
- 同时我们也将之前的所有测试数据合并到了一个测试中的表格中。
- 现在我们的新测试阿都通过了:
- 这种表格驱动的测试在Go语言中很常见的。 我们很容易向表格添加新的测试数据, 并且后面的测试逻辑也没有冗余, 这样我们可以有更多的精力地完善错误信息。
- 失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。 和其他编程语言或测试框架的assert断言不同, t.Errorf调用也没有引起panic异常或停止测试的执行。 即使表格中前面的数据导致了测试的失败, 表格后面的测试数据依然会运行测试, 因此在一个测试中我们可能了解多个失败的信息。
- 如果我们真的需要停止测试, 或许是因为初始化失败或可能是早先的错误导致了后续错误等原因, 我们可以使用t.Fatal或t.Fatalf停止当前测试函数。 它们必须在和测试函数同一个goroutine内调用。
- 测试失败的信息一般的形式是“f(x) = y, want z”, 其中f(x)解释了失败的操作和对应的输出, y是实际的运行结果, z是期望的正确的结果。 就像前面检查回文字符串的例子, 实际的函数用于f(x)部分。 如果显示x是表格驱动型测试中比较重要的部分, 因为同一个断言可能对应不同的表格项执行多次。 要避免无用和冗余的信息。 在测试类似IsPalindrome返回布尔类型的函数时, 可以忽略并没有额外信息的z部分。 如果x、 y或z是y的长度, 输出一个相关部分的简明总结即可。 测试的作者应该要努力帮助程序员诊断测试失败的原因。
1. 随机测试
- 表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。 另一种测试思路是随机测试, 也就是通过构造更广泛的随机输入来测试探索函数的行为。
- 那么对于一个随机的输入, 我们如何能知道希望的输出结果呢? 这里有两种处理策略。 第一个是编写另一个对照函数, 使用简单和清晰的算法, 虽然效率较低但是行为和要测试的函数是一致的, 然后针对相同的随机输入检查两者的输出结果。 第二种是生成的随机输入的数据遵循特定的模式, 这样我们就可以知道期望的输出的模式。
- 下面的例子使用的是第二种方法: randomPalindrome函数用于随机生成回文字符串。
- 虽然随机测试会有不确定因素, 但是它也是至关重要的, 我们可以从失败测试的日志获取足够的信息。 在我们的例子中, 输入IsPalindrome的p参数将告诉我们真实的数据, 但是对于函数将接受更复杂的输入, 不需要保存所有的输入, 只要日志中简单地记录随机数种子即可
( 像上面的方式) 。 有了这些随机数初始化种子, 我们可以很容易修改测试代码以重现失败的随机测试。 - 通过使用当前时间作为随机种子, 在整个过程中的每次运行测试命令时都将探索新的随机数据。 如果你使用的是定期运行的自动化测试集成系统, 随机测试将特别有价值
2. 测试一个命令
- 对于测试包 go test 是一个的有用的工具, 但是稍加努力我们也可以用它来测试可执行程序。 如果一个包的名字是 main, 那么在构建时会生成一个可执行程序, 不过main包可以作为一个包被测试器代码导入。
- 让我们为2.3.2节的echo程序编写一个测试。 我们先将程序拆分为两个函数: echo函数完成真正的工作, main函数用于处理命令行输入参数和echo可能返回的错误。
- gopl.io/ch11/echo
- 在测试中我们可以用各种参数和标标志调用echo函数, 然后检测它的输出是否正确, 我们通过增加参数来减少echo函数对全局变量的依赖。 我们还增加了一个全局名为out的变量来替代直接使用os.Stdout, 这样测试代码可以根据需要将out修改为不同的对象以便于检查。 下面就是echo_test.go文件中的测试代码:
- 要注意的是测试代码和产品代码在同一个包。 虽然是main包, 也有对应的main入口函数, 但是在测试的时候main包只是TestEcho测试函数导入的一个普通包, 里面main函数并没有被导出, 而是被忽略的。
- 通过将测试放到表格中, 我们很容易添加新的测试用例。 让我通过增加下面的测试用例来看看失败的情况是怎么样的:
- go test 输出如下:
- 错误信息描述了尝试的操作( 使用Go类似语法) , 实际的结果和期望的结果。 通过这样的错误信息, 你可以在检视代码之前就很容易定位错误的原因。
- 要注意的是在测试代码中并没有调用log.Fatal或os.Exit, 因为调用这类函数会导致程序提前退出; 调用这些函数的特权应该放在main函数中。 如果真的有意外的事情导致函数发生panic异常, 测试驱动应该尝试用recover捕获异常, 然后将当前测试当作失败处理。 如果是可预期的错误, 例如非法的用户输入、 找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。 幸运的是( 上面的意外只是一个插曲) , 我们的echo示例是比较简单的也没有需要返回非空error的情况。
3. 白盒测试
- 一种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。 黑盒测试只需要测试包公开的文档和API行为, 内部实现对测试代码是透明的。 相反, 白盒测试有访问包内部函数和数据结构的权限, 因此可以做到一下普通客户端无法实现的测试。 例如, 一个白盒测试可以在每个操作之后检测不变量的数据类型。 ( 白盒测试只是一个传统的名称, 其实称为clear box测试会更准确。 )
- 黑盒和白盒这两种测试方法是互补的。 黑盒测试一般更健壮, 随着软件实现的完善测试代码很少需要更新。 它们可以帮助测试者了解真是客户的需求, 也可以帮助发现API设计的一些不足之处。 相反, 白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖。
- 我们已经看到两种测试的例子。 TestIsPalindrome测试仅仅使用导出的IsPalindrome函数, 因此这是一个黑盒测试。 TestEcho测试则调用了内部的echo函数, 并且更新了内部的out包级变量, 这两个都是未导出的, 因此这是白盒测试。
- 当我们准备TestEcho测试的时候, 我们修改了echo函数使用包级的out变量作为输出对象, 因此测试代码可以用另一个实现代替标准输出, 这样可以方便对比echo输出的数据。 使用类似的技术, 我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。 使用伪对象的好处是我们可以方便配置, 容易预测, 更可靠, 也更容易观察。 同时也可以避免一些不良的副作用, 例如更新生产数据库或信用卡消费行为。
- 下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。 当用户使用了超过90%的存储配额之后将发送提醒邮件。
- gopl.io/ch11/storage1
- 我们想测试这个代码, 但是我们并不希望发送真实的邮件。 因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。
- gopl.io/ch11/storage2
- 现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。 它只是简单记录要通知的用户和邮件的内容。
- 这里有一个问题: 当测试函数返回后, CheckQuota将不能正常工作, 因为notifyUsers依然使用的是测试函数的伪发送邮件函数( 当更新全局对象的时候总会有这种风险) 。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响, 要确保所有的执行路径后都能恢复, 包括测试失败或panic异常的情形。 在这种情况下, 我们建议使用defer语句来延后执行处理恢复的代码。
- 这种处理模式可以用来暂时保存和恢复所有的全局变量, 包括命令行标志参数、 调试选项和优化参数; 安装和移除导致生产代码产生一些调试信息的钩子函数; 还有有些诱导生产代码进入某些重要状态的改变, 比如超时、 错误, 甚至是一些刻意制造的并发行为等因素。
- 以这种方式使用全局变量是安全的, 因为go test命令并不会同时并发地执行多个测试。
待续…