[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
    5
    func TestTruth(t *testing.T) {
    if true != true {
    t.Fatal("The world is crumbling")
    }
    }
  • 测试包中创建两个变量: gotwant,它们分别表示要测试的值以及期望的值。

    run.go:

    1
    2
    3
    4
    5
    6
    package awesomeProject

    func Greeting(s string) string {
    return "Hello " + s
    }

    run_test.go:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package 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"
    FAIL

    got want 模式很有用,它能快速地发现测试失败的原因,另外,通过测试失败时显示有帮助的错误信息,可以迅速定位问题

    运行表格驱动测试

    通常,函数和方法的响应随收到的输入而异,在这种情况下,如果每个测试只使用一个值,将导致大量重复的代码。如以下代码,支持向 Greeting 传送区域,以显示相应语言的问候语

run.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package awesomeProject

import "testing"

type GreetingTest struct {
name string
locale string
want string
}

var greetingTests = []GreetingTest{
{"dirname", "en-US", "Hello dirname"},
{"dirname", "fr-FR", "Bonjour dirname"},
{"dirname", "it-IT", "Ciao dirname"},
}

func TestGreeting(t *testing.T) {
for _, test := range greetingTests {
got := Greeting(test.name, test.locale)
if got != test.want {
t.Errorf("Greeting(%s,%s) = %v, want %v", test.name, test.locale, got, test.want)
}
}
}

表格驱动测试模式,能同时测试很多条件

基准测试

Go 提供了功能强大的基准测试框架,能够确定完成特定任务性能最佳的方式是哪一种,如下是字符串拼接的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package awesomeProject

import (
"bytes"
"strings"
)

func StringFromAssignment(j int) string {
var s string
for i := 0; i < j; i++ {
s += "a"
}
return s
}

func StringFromAppendJoin(j int) string {
var s []string
for i := 0; i < j; i++ {
s = append(s, "a")
}
return strings.Join(s, "")
}

func StringFromBuffer(j int) string {
var buffer bytes.Buffer
for i := 0; i < j; i++ {
buffer.WriteString("a")
}
return buffer.String()
}

test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package awesomeProject

import "testing"

func BenchmarkStringFromAppendJoin(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromAppendJoin(100)
}
}

func BenchmarkStringFromAssignment(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromAssignment(100)
}
}

func BenchmarkStringFromBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
StringFromBuffer(100)
}
}

基准测试以关键字 Benchmark 开头,它能反复地运行函数,从而建立基准,无需指定函数运行的次数,因为基准测试框架将通过调整来获得可靠的数据及,测试完成后,将生成一个报告,并指出每次操作好用了多少 ns

执行结果:

1
2
3
4
5
6
7
goos: windows
goarch: amd64
pkg: awesomeProject
BenchmarkStringFromAppendJoin-12 445722 2279 ns/op
BenchmarkStringFromAssignment-12 293458 4260 ns/op
BenchmarkStringFromBuffer-12 1735819 713 ns/op
PASS

由此可以看出,赋值的性能是最差的,使用 join 的剧中,而使用 buffer 是最快的

提供测试覆盖率

测试覆盖率是度量代码测试详尽程度的指标,它指出了被测试执行了的代码所在的百分比值,假设新增一个函数

1
2
3
4
5
6
7
8
9
package awesomeProject

func Greeting(s string) string {
return "Hello " + s
}

func Farewell(s string) string{
return "test"
}

test:

1
2
3
4
5
6
7
8
9
10
11
package 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)
}
}

上诉的代码没有对新函数 Farewell 进行测试,go test 命令提供了 -cover,可指出测试的覆盖率

上述代码测试结果:

1
2
3
4
=== RUN   TestGreeting
--- PASS: TestGreeting (0.00s)
PASS
coverage: 50.0% of statements

结果表明,测试只覆盖了 50% 的代码,最佳目标是实现 100%,但实际上这样的目标并非总能实现,因为对于有些代码,要对其进行测试很难,建议时不时检查一下覆盖率即可

问题列表

  • 真的需要编写测试吗,编写的代码很少,而且没有时间

    从长远看,编写测试物有所值,即便项目很小,虽然测试看起来是负担,但实际上有很多好处,因为这样可以确保代码实现正确,且发现修改过程中引入的衰退

  • 该以什么样的频率运行测试

    理想情况下,每次提交代码前都应运行测试

  • 应达到多少的测试覆盖率

    实现100%的测试率是一个值得为之努力的目标,但对于大型项目而言,这几乎不可能,基本上达到 80% 左右的测试覆盖率就好了


All articles in this blog adopt the CC BY-SA 4.0 agreement unless otherwise stated. Please indicate the source for reprinting!