测试驱动开发与 golang 单元测试

2019-12-20 18:43:52   最后更新: 2019-12-20 18:43:52   访问数量:182




在现代程序设计中,测试显得越来越重要,未经测试就在线上供用户使用其后果很可能是灾难性的

 

 

软件开发界泰斗 Kent Beck 先生甚至在《Test Driven Development: By Example》一书中提出了著名的测试驱动开发理论 -- TDD

 

 

众所周知,在盖房子前,先拉起基准线,再比照着线来砌砖是一个好习惯,而在软件开发中,TDD 就是这个基准线,他要求在开发工作开始前,先根据用户需求编写测试用例,再在开发的过程中不断用测试用例校验代码,直到完全通过即意味着开发完成

同时,历史的所有测试用例都持续保留,可以保证新增需求对老功能影响的可控性

 

优点

  1. 提升工程质量 -- 丰富的测试用例让开发者的开发更加专注,能够做到有的放矢,从而减轻压力与程序设计过程中的不可控因素
  2. 提升开发效率 -- 敏捷开发变得可行
  3. 更容易重构 -- 完整的测试用例十分便于回归测试,在重构过程中,丰富的回归测试让重构过程更加可控

 

缺点

  1. 可能造成开发人员将注意力过度集中于单元测试用例,而忽略更加长期的规划
  2. 开发过程需要额外维护所有单元测试用例与回归测试用例的正确性,增大开发成本,尤其是在实际工程开发中,需求总是会发生变化,这会造成测试用例的频繁更改,更加令人难以维护
  3. GUI、web 页面等难以编写测试用例

 

在很多企业中都或多或少的应用着 TDD 的思想,而其本质上是企业对于软件测试的重视,在开发过程中,不断的测试,让问题尽早的暴露和扼杀,避免问题的扩散,降低不可控性

现代编程语言中,很多都集成了测试工具,例如 golang 中,就有 testing 包提供一系列测试工具

通过 go test 命令就可以实现测试用例的执行,通过不同的参数还可以进行例如压测、并发测试等测试功能

下面就来详细介绍一下

 

单元测试是最为常见和常用的测试方法

只要在项目文件中写入下面的方法:

func TestXxx(*testing.T) { // 测试函数体 }

 

 

然后执行:

go test .

 

就可以看到编译、运行后的测试结果了

 

示例

测试通过

我们编写一个斐波那契数列运算的函数:

func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }

 

 

编写单元测试代码:

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

 

测试通过

 

测试失败

我们稍稍修改一下代码:

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

 

显然,测试失败了

 

上面的例子中,我们使用到了 testing.T 中的 Errorf 方法,他打印出了错误信息,但事实上,他并不会中断程序的执行

而 testing.T 类提供了几个十分常用的报告方法

 

testing.T

testing.T 的结构定义如下:

type T struct { common isParallel bool context *testContext // For running tests and subtests. }

 

 

common 是一个 struct,他为 T 类型提供了所有的报告方法

 

common 提供的报告方法

testing.T 的报告方法
方法名声明说明
LogLog(args ...interface{})输出信息
LogfLogf(format string, args ...interface{})格式化输出信息
FailFail()提示用户测试失败并继续
FailNowFailNow()提示用户测试失败并中止测试(通过调用 runtime.Goexit())
ErrorError(args ...interface{})提示用户测试错误并打印信息,通过调用 Log + Fail 实现
ErrorfErrorf(format string, args ...interface{})Error 方法的格式化输出版本
SkipNowSkipNow()跳出测试(通过调用 runtime.Goexit())
SkipSkip(args ...interface{})打印信息并退出测试,通过调用 Log 与 SkipNow 实现
SkipfSkipf(format string, args ...interface{})Skip 的格式化输出版本
FatalFatal(args ...interface{})输出日志、提示用户测试失败并退出,通过调用 Log 与 FailNow 实现
FatalfFatalf(format string, args ...interface{})Fatal 的格式化输出版本

 

掌握了上面的内容,你就可以为你的代码编写合适的测试用例了

但是,有的时候你想要像函数调用一样嵌套多个单元测试,或者想在若干个测试开始前或结束后做一些事情,这在 go 语言中有着很好的支持

golang 1.7 版本开始,引入了一个新特性 -- 子测试

 

示例

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> }

 

 

执行

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" 的子测试。

 

子测试并发执行 -- t.Parallel()

很多情况下,我们并不想等着若干个子测试一个个顺次执行,而是希望能够让他们相互并发执行,这时 t.Parallel() 就派上用场了

当然,t.Parallel() 并不仅仅能够应用在子测试中,任何几个测试函数中,只要调用了 t.Parallel(),他们之间都会并发执行

func TestGroupedParallel(t *testing.T) { for _, tc := range tests { tc := tc // capture range variable t.Run(tc.Name, func(t *testing.T) { t.Parallel() ... }) } }

 

 

子测试让我们能够嵌套测试函数,在若干个测试函数之前、之后或之间进行一些操作

但我们是否可以定义,无论在什么情况下,只要测试函数执行,他前后就必须执行一些操作呢?

golang 用 TestMain 可以实现这样的特性

func TestMain(m *testing.M)

 

只要测试文件中包含该函数,那么,无论执行测试文件中的哪个函数,都会先去运行 TestMain 函数

在 TestMain 函数中,通过 m.Run() 就可以调用本次预期将会执行的测试函数

不难看出,这是一个面向切面编程思想的应用

 

示例

func TestMain(m *testing.M) { // do someting setup exitCode := m.Run() os.Exit(exitCode) // do something teardow }

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 






测试      单元测试      test      tdd      子测试      testmain     


京ICP备15018585号