[Go 入门] 第十四章 测试和性能
Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除
阅读完本文后, 可以对 Go 代码进行测试和基准测试, 本文内容:
测试的重要性
测试软件程序可能是软件开发人员能够做的最重要的事情。通过测试代码的功能,开发人员能够在很大程度上确定程序是否是有效的。另外,每次修改代码后,可以运行测试,确认没有引入 Bug 和衰退。通过测试软件,还能确定程序按照预期在工作
通常,软件测试是从概述功能的用户故事或规范衍生而来的。
常用的测试有很多种:
- 单元测试
- 功能测试
- 集成测试
单元测试
单元测试针对一小部分代码,并独立地对它进行测试。通常,这一小部分代码可能是单个函数,而要测试的是其输入和输出。典型的单元测试可能指出,如果给函数 x 提供这些值,它应返回这个值。在确认程序最小的构建按期望的方式运行方面,这种测试很有用。在程序增大和变化过程中,单元测试时发现衰退,因为它们测试的是程序的最小组成部分。
集成测试
集成测试通常测试的是应用程序各部分协同工作的情况。如果说单元测试检查的是程序的最小组成部分,那么集成测试检查的就是应用程序各个组件协同工作的情况。集成测试还检查诸如网络调用和数据库连接等方面,以确保整个系统按期望那样工作。通常,集成测试比单元测试更难编写,因为这些测试需要评估应用程序依赖的各个部分。
功能测试
功能测试通常被称为端到端测试或由外向内的测试。这些测试从最终用户的角度何时软件按期望的那样工作,它们评估从外部看到的程序的运行状况,而不关心软件内部的工作原理。对用户来说,功能测试可能是最重要的测试。下面是一些功能测试的例子:
- 测试命令行工具,确定在用户提供特定的输入时,它将显示特定的输出
- 对网页运行自动化测试
- 对 API 运行从外到内的测试,并检查响应代码和报头
测试驱动开发
很多开发人员都提倡采用测试驱动开发(TDD)。这种做法从测试的角度考虑新功能,先编写测试来描述代码片段的功能,再着手编写代码。这有很多优点:
- 有助于描述代码设计,因为考虑清楚代码片段的工作原理后,可改善代码设计
- 有助于提供有关功能工作原理的定义
- 未来可使用现成的测试来确定没有发生衰退
- 可使用现成的测试来核实正确地实现了代码
通过采用TDD,工程师可以改善设计,并根据确保测试得以通过来确认代码是有效的
testing 包
为支持测试,Go 语言在标准库中提供了 testing
包,它还支持命令 go。与 Go 语言的其他众多方面一样,testing
包也有约定
Go 测试与其测试的代在一起,测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀
_test
测试为名称以单词 Test 打头的函数,如下检查的是布尔值
true
是否与它自身相等1
2
3
4
5func TestTruth(t *testing.T) {
if true != true {
t.Fatal("The world is crumbling")
}
}测试包中创建两个变量:
got
和want
,它们分别表示要测试的值以及期望的值。run.go:
1
2
3
4
5
6package awesomeProject
func Greeting(s string) string {
return "Hello " + s
}run_test.go:
1
2
3
4
5
6
7
8
9
10
11package awesomeProject
import "testing"
func TestGreeting(t *testing.T) {
got := Greeting("dirname")
want := "Hello dirname"
if got != want {
t.Fatalf("Expected %q, got %q", got, want)
}
}如果是
go1.11版本以上
,遇到missing dot in first path element
错误,请先执行go mod init 域名/组名/项目名(比如code.be.mingbai.com/tools/soa)
运行后,可以看到测试通过
1
2
3=== RUN TestGreeting
--- PASS: TestGreeting (0.00s)
PASS如果将 want 修改为 “Hi dirname”,将导致测试失败
1
2
3
4=== RUN TestGreeting
--- FAIL: TestGreeting (0.00s)
run_test.go:9: Expected "Hello dirname", got "Hi dirname"
FAILgot want 模式很有用,它能快速地发现测试失败的原因,另外,通过测试失败时显示有帮助的错误信息,可以迅速定位问题
运行表格驱动测试
通常,函数和方法的响应随收到的输入而异,在这种情况下,如果每个测试只使用一个值,将导致大量重复的代码。如以下代码,支持向 Greeting 传送区域,以显示相应语言的问候语
run.go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package awesomeProject
func translate(s string) string{
switch s {
case "en-US":
return "Hello "
case "fr-FR":
return "Bonjour "
case "it-IT":
return "Ciao "
default:
return "Hello"
}
}
func Greeting(name, local string) string {
salutation := translate(local)
return salutation + name
}
run_test.go:
1 |
|
表格驱动测试模式,能同时测试很多条件
基准测试
Go 提供了功能强大的基准测试框架,能够确定完成特定任务性能最佳的方式是哪一种,如下是字符串拼接的函数
1 |
|
test:
1 |
|
基准测试以关键字 Benchmark
开头,它能反复地运行函数,从而建立基准,无需指定函数运行的次数,因为基准测试框架将通过调整来获得可靠的数据及,测试完成后,将生成一个报告,并指出每次操作好用了多少 ns
执行结果:
1 |
|
由此可以看出,赋值的性能是最差的,使用 join 的剧中,而使用 buffer 是最快的
提供测试覆盖率
测试覆盖率是度量代码测试详尽程度的指标,它指出了被测试执行了的代码所在的百分比值,假设新增一个函数
1 |
|
test:
1 |
|
上诉的代码没有对新函数 Farewell
进行测试,go test 命令提供了 -cover,可指出测试的覆盖率
上述代码测试结果:
1 |
|
结果表明,测试只覆盖了 50% 的代码,最佳目标是实现 100%,但实际上这样的目标并非总能实现,因为对于有些代码,要对其进行测试很难,建议时不时检查一下覆盖率即可
问题列表
真的需要编写测试吗,编写的代码很少,而且没有时间
从长远看,编写测试物有所值,即便项目很小,虽然测试看起来是负担,但实际上有很多好处,因为这样可以确保代码实现正确,且发现修改过程中引入的衰退
该以什么样的频率运行测试
理想情况下,每次提交代码前都应运行测试
应达到多少的测试覆盖率
实现100%的测试率是一个值得为之努力的目标,但对于大型项目而言,这几乎不可能,基本上达到 80% 左右的测试覆盖率就好了