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

2022-06-27 14:15:27 浏览数 (1)

1. 引言

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

2. 测试驱动开发

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

众所周知,在盖房子前,先拉起基准线,再比照着线来砌砖是一个好习惯,而在软件开发中,TDD 就是这个基准线,他要求在开发工作开始前,先根据用户需求编写测试用例,再在开发的过程中不断用测试用例校验代码,直到完全通过即意味着开发完成。 同时,历史的所有测试用例都持续保留,可以保证新增需求对老功能影响的可控性。

2.1. 优点

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

2.2. 缺点

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

0 人点赞