22.测试

测试

在包目录内,所有以_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。

测试文件可以放其它源文件同一个目录。

  • 如果测试针对的是未导出的 API,那么测试文件和其它源代码文件放在同一个包 package exam
  • 如果测试是用户使用这套 API 的方式,可以对测试文件的包名加后缀 package exam_test

*_test.go文件中,有三种类型的函数

  • 测试函数 (Test)
  • 基准测试函数(Bench)
  • 示例函数 (Example)

测试函数

用于测试程序的一些逻辑行为是否正确,go test 命令会调用这些测试函数并报告测试结果。

1
2
3
import "testing"
func TestFuncName(t *testing.T) {
}

参考上面的格式, 以 Test 为函数名前缀,后面的FuncName首字母必须大写,参数类型必须是 *testing.T

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 测试全部文件
go test -v
# 测试单个文件
go test -v cal_test.go cal.go
# 测试单个方法
go test -v <file_name.go> -run TestAddUpper
# 测试以 French 和 Canal 为前缀的函数
go test -v -run="French|Canal" 
# === RUN TestFrenchPalindrome
# === RUN TestCanalPalindrome
# 测试所有子目录
go test -v ./...
  • -v 用于打印每个测试函数的名字和运行时间
  • -run对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会执行
  • -cover 覆盖率
  • -coverprofile 生成覆盖详情
1
2
go test -coverprofile c.out
go tool cover -html c.out 

image-20220424103652008

[命令行工具参考官网文档][https://pkg.go.dev/cmd/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
func TestIsPalindrome(t *testing.T) {
    var tests = []struct {
        input string
        want  bool
    }{
        {"", true},
        {"a", true},
        {"aa", true},
        {"ab", false},
        {"kayak", true},
        {"detartrated", true},
        {"A man, a plan, a canal: Panama", true},
        {"Evil I did dwell; lewd did I live.", true},
        {"Able was I ere I saw Elba", true},
        {"été", true},
        {"Et se resservir, ivresse reste.", true},
        {"palindrome", false}, // non-palindrome
        {"desserts", false},   // semi-palindrome
    }
    for _, test := range tests {
        if got := IsPalindrome(test.input); got != test.want {
            t.Errorf("IsPalindrome(%q) = %v", test.input, got)
        }
    }
}

func IsPalindrome(str string) bool {
  // ...
}

模拟 webserver 应答

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// mockServer 模拟服务器
func mockServer() *httptest.Server {
	var f http.HandlerFunc
	f = func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, "welcome")
	}
	return httptest.NewServer(f)
}

// TestNewRequest 模拟发起请求
func TestNewRequest(t *testing.T) {
	server := mockServer()
	defer server.Close()

	req := httptest.NewRequest("GET", server.URL, nil)
	w := httptest.NewRecorder()

	server.Config.Handler.ServeHTTP(w, req)
	require.EqualValues(t, w.Result().StatusCode, http.StatusOK)
	require.EqualValues(t, "welcome", w.Body.String())
}

示例函数

属于通过 godoc 生成文档的一部分。

以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。和普通测试的区别,示例函数没有函数参数和返回值。

示例函数有三个用处。

  1. 作为文档, 根据示例函数的后缀名部分,godoc这个web文档服务器会将示例函数关联到某个具体函数或包本身
  2. go test执行测试的时候也会运行示例函数测试。如果示例函数内含有类似上面例子中的// Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。
  3. 提供一个真实的演练场, 在线编辑和运行示例函数
1
2
3
4
5
6
7
func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
    fmt.Println(IsPalindrome("palindrome"))
    // Output:
    // true
    // false
}
1
2
# 安装文档
go get -u golang.org/x/tools/cmd/godoc

子测试及平行执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func TestSubTest(t *testing.T) {
	data := []struct {
		name   string
		result bool
	}{
		{"123", true},
		{"246", false},
	}

	for _, v := range data {
		tf := func(t *testing.T) {
      t.Parallel()
			t.Log(v)
		}
		t.Run(v.name, tf)
	}
}
1
2
# 仅仅想执行名为 246 的子测试
go test -v -run SubTest/246

默认情况下,go test 在不同包之间是并行执行,在每个包内部是串行执行。使用 t.Parallel()让当前函数开启并行。

基准测试

fmt.Sprint 和 fmt.Sprintf 哪个性能更佳呢?

为了保证测试结果准确,必须保证电脑没有执行其它事务。如果要执行长时间的测试,一定不要打开浏览器上网,或去看在线视频。

Go 语言做 benchmark 的时候,会让编译器重新编译代码,编译器会把没有用处的死代码拿掉。也就是说,如果编译器发现某个函数并没有修改任何内容,或虽然返回了某个值,却没有保存起来,编译器就会认证没必要浪费时间去调用这个函数,因为它对程序的结果不会产生任何影响。

通过下面的命令执行测试, -run可以是正则表达式,这里写 none 表示不执行任何 Test 函数,仅执行 benchmark 函数。-bench 标志的正则表达式写出 . 表示匹配所有。通过-benchtime 设置测试时间为 3 秒。

benchmark 也有子测试,子测试的主要意义并不在于并行,而在于可以更加细致地测评。

1
2
# none 没有特殊的意义,仅仅用来确保没有任何函数与它匹配
go test -run none -bench . -benchtime 3s -banchmem

测试结果大概长这样,分别表示

  • 测试函数名-CPU 核心数
  • 执行次数
  • 执行时间(纳秒),看图中,将小数点左移6 位,该测试大概 245 毫秒一次
  • 内存
  • 分配次数

image-20220410005542697

验证测评结果

测评的结果,总是应该在自己所能理解的范围之内,如果跟自己想的差太远,那一定要把其中的道理弄清楚。这并不意味着代码写错了,但还是应该尽量想办法拿到准确的测评结果。

测试驱动开发

测试驱动开发需要你做一些小步骤,每一个实现都会感觉微不足道。真正的价值不在于步骤本身,而在于最终产品,即使做了一个微不足道的更改,这肯定会起作用。

三个步骤

  • 红色,通常终端会红色提示错误。先编写测试代码,然后实现业务函数,期间可能无法通过测试。
  • 绿色,通过终端会绿色表示成功。当业务函数通过测试代码。
  • 重构,再不添加任何功能的情况下改进其结构,如果没有这一步,您的代码将很快退化为经过充分测试但难以理解的混乱。
    • 如何在不改变功能的情况下使这段代码更好地表达其意图 ( 可理解性 ) ?
    • 许多重构不仅涉及被测代码,还涉及测试本身。您需要花费更多的时间来修复测试而不是改进代码。
    • 如果您的被测单元提供多个分支,则值得考虑拆分成多个单元。

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要对字符串进行全字匹配,而是针对那些在项目的发展中是比较稳定不变的子串。很多时候值得花力气来编写一个从复杂输出中提取用于断言的必要信息的函数,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试。

Licensed under CC BY-NC-SA 4.0
本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy