1. 引言
在现代程序设计中,测试显得越来越重要,未经测试就在线上供用户使用其后果很可能是灾难性的。
2. 测试驱动开发
软件开发界泰斗 Kent Beck 先生甚至在《Test Driven Development: By Example》一书中提出了著名的测试驱动开发理论 — TDD。
众所周知,在盖房子前,先拉起基准线,再比照着线来砌砖是一个好习惯,而在软件开发中,TDD 就是这个基准线,他要求在开发工作开始前,先根据用户需求编写测试用例,再在开发的过程中不断用测试用例校验代码,直到完全通过即意味着开发完成。 同时,历史的所有测试用例都持续保留,可以保证新增需求对老功能影响的可控性。
2.1. 优点
- 提升工程质量 — 丰富的测试用例让开发者的开发更加专注,能够做到有的放矢,从而减轻压力与程序设计过程中的不可控因素
- 提升开发效率 — 敏捷开发变得可行
- 更容易重构 — 完整的测试用例十分便于回归测试,在重构过程中,丰富的回归测试让重构过程更加可控
2.2. 缺点
- 可能造成开发人员将注意力过度集中于单元测试用例,而忽略更加长期的规划
- 开发过程需要额外维护所有单元测试用例与回归测试用例的正确性,增大开发成本,尤其是在实际工程开发中,需求总是会发生变化,这会造成测试用例的频繁更改,更加令人难以维护
- GUI、web 页面等难以编写测试用例
3. golang 测试工具
在很多企业中都或多或少的应用着 TDD 的思想,而其本质上是企业对于软件测试的重视,在开发过程中,不断的测试,让问题尽早的暴露和扼杀,避免问题的扩散,降低不可控性。 现代编程语言中,很多都集成了测试工具,例如 golang 中,就有 testing 包提供一系列测试工具。 通过 go test 命令就可以实现测试用例的执行,通过不同的参数还可以进行例如压测、并发测试等测试功能。 下面就来详细介绍一下。
4. 单元测试
单元测试是最为常见和常用的测试方法。 只要在项目文件中写入下面的方法:
代码语言:javascript复制func TestXxx(*testing.T) {
// 测试函数体
}
然后执行:
go test .
就可以看到编译、运行后的测试结果了。
4.1. 示例
4.1.1. 测试通过
我们编写一个斐波那契数列运算的函数:
代码语言:javascript复制func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) Fib(n-2)
}
编写单元测试代码:
代码语言:javascript复制func TestFib(t *testing.T) {
var (
in = 7
expected = 13
)
actual := Fib(in)
if actual != expected {
t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
}
}
执行 go test . 输出了:
ok chapter09/testing 0.007s
测试通过。
4.1.2. 测试失败
我们稍稍修改一下代码:
代码语言:javascript复制func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) Fib(n-1)
}
执行 go test . 可以看到:
—- FAIL: TestSum (0.00s) t_test.go:16: Fib(10) = 64; expected 13 FAIL FAIL chapter09/testing 0.009s
显然,测试失败了。
5. testing.T 中的报告方法
上面的例子中,我们使用到了 testing.T 中的 Errorf 方法,他打印出了错误信息,但事实上,他并不会中断程序的执行。 而 testing.T 类提供了几个十分常用的报告方法。
5.1. testing.T
testing.T 的结构定义如下:
代码语言:javascript复制type T struct {
common
isParallel bool
context *testContext // For running tests and subtests.
}
common 是一个 struct,他为 T 类型提供了所有的报告方法。
5.2. common 提供的报告方法
testing.T 的报告方法
方法名 | 声明 | 说明 |
---|---|---|
Log | Log(args …interface{}) | 输出信息 |
Logf | Logf(format string, args …interface{}) | 格式化输出信息 |
Fail | Fail() | 提示用户测试失败并继续 |
FailNow | FailNow() | 提示用户测试失败并中止测试(通过调用 runtime.Goexit()) |
Error | Error(args …interface{}) | 提示用户测试错误并打印信息,通过调用 Log Fail 实现 |
Errorf | Errorf(format string, args …interface{}) | Error 方法的格式化输出版本 |
SkipNow | SkipNow() | 跳出测试(通过调用 runtime.Goexit()) |
Skip | Skip(args …interface{}) | 打印信息并退出测试,通过调用 Log 与 SkipNow 实现 |
Skipf | Skipf(format string, args …interface{}) | Skip 的格式化输出版本 |
Fatal | Fatal(args …interface{}) | 输出日志、提示用户测试失败并退出,通过调用 Log 与 FailNow 实现 |
Fatalf | Fatalf(format string, args …interface{}) | Fatal 的格式化输出版本 |
6. 子测试
掌握了上面的内容,你就可以为你的代码编写合适的测试用例了。 但是,有的时候你想要像函数调用一样嵌套多个单元测试,或者想在若干个测试开始前或结束后做一些事情,这在 go 语言中有着很好的支持。 golang 1.7 版本开始,引入了一个新特性 — 子测试。
6.1. 示例
代码语言:javascript复制func TestFoo(t *testing.T) {
// <setup code>
t.Run("A=1", func(t *testing.T) { ... })
t.Run("A=2", func(t *testing.T) { ... })
t.Run("B=1", func(t *testing.T) { ... })
// <tear-down code>
}
6.2. 执行
go test -run ’’ # Run 所有测试。 go test -run Foo # Run 匹配 "Foo" 的顶层测试,例如 "TestFoo"、"TestFooBar"。 go test -run Foo/A= # 匹配顶层测试 "Foo",运行其匹配 "A=" 的子测试。 go test -run /A=1 # 运行所有匹配 "A=1" 的子测试。
6.3. 子测试并发执行 — t.Parallel()
很多情况下,我们并不想等着若干个子测试一个个顺次执行,而是希望能够让他们相互并发执行,这时 t.Parallel() 就派上用场了。 当然,t.Parallel() 并不仅仅能够应用在子测试中,任何几个测试函数中,只要调用了 t.Parallel(),他们之间都会并发执行。
代码语言:javascript复制func TestGroupedParallel(t *testing.T) {
for _, tc := range tests {
tc := tc // capture range variable
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
...
})
}
}
7. TestMain
子测试让我们能够嵌套测试函数,在若干个测试函数之前、之后或之间进行一些操作。 但我们是否可以定义,无论在什么情况下,只要测试函数执行,他前后就必须执行一些操作呢? golang 用 TestMain 可以实现这样的特性。
func TestMain(m *testing.M)
只要测试文件中包含该函数,那么,无论执行测试文件中的哪个函数,都会先去运行 TestMain 函数。 在 TestMain 函数中,通过 m.Run() 就可以调用本次预期将会执行的测试函数。 不难看出,这是一个面向切面编程思想的应用。
7.1. 示例
代码语言:javascript复制func TestMain(m *testing.M) {
// do someting setup
exitCode := m.Run()
os.Exit(exitCode)
// do something teardow
}