Go语言学习笔记之 单元测试

Go语言学习笔记之 单元测试

作为一名合格的开发者,不应该在程序开发完之后才开始写测试代码。使用 Go 语言的测试框架,可以在开发的过程中就进行单元测试基准测试

和 go build 命令类似,go test 命 令可以用来执行写好的测试代码,需要做的就是遵守一些规则来写测试。而且,可以将测试无缝 地集成到代码工程和持续集成系统里。

1.单元测试

单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标 代码在给定的场景下,有没有按照期望工作。一个场景是正向路经测试,就是在正常执行的情况 下,保证代码不产生错误的测试。这种测试可以用来确认代码可以成功地向数据库中插入一条工作记录。

另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。这种场景下的测试可能是对数据库进行查询时没有找到任何结果,或者对数据库做了无效的更新。在这两种情况下,测试都要验证确实产生了错误,且产生的是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的。

在 Go 语言里有几种方法写单元测试。基础测试(basic test)只使用一组参数和结果来测试 一段代码。 **表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。**也 可以使用一些方法来模仿(mock)测试代码需要使用到的外部资源,如数据库或者网络服务器。 这有助于让测试在没有所需的外部资源可用的时候,模拟这些资源的行为使测试正常进行。最后,在构建自己的网络服务时,有几种方法可以在不运行服务的情况下,调用服务的功能进行测试。

1.1 基础单元测试

// 这个示例程序展示如何写基础单元测试
package main

import (
	"net/http"
	"testing"
)

const checkMark = "\u2713"// 对号(√)
const ballotX = "\u2717"// 叉号(×)

// TestDownload 确认 http 包的 Get 函数可以下载内容
func TestDownload(t *testing.T) {
	url := "http://www.baidu.com"
	statusCode := 200
	t.Log("")

	t.Log("Given the need to test downloading content.")
	{
		t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
			url, statusCode)
		{
			resp, err := http.Get(url)
			if err != nil {
				t.Fatal("\t\tShould be able to make the Get call.", ballotX, err)
			}
			t.Log("\t\tShould be able to make the Get call.", checkMark)

			defer resp.Body.Close()

			if resp.StatusCode == statusCode {
				t.Logf("\t\tShould receive a \"%d\" status. %v",
					statusCode, checkMark)
			} else {
				t.Errorf("\t\tShould receive a \"%d\" status. %v %v",
					statusCode, ballotX, resp.StatusCode)
			}
		}
	}
}

结果

$ go test -v
=== RUN   TestDownload
    listing01_test.go:17: Given the need to test downloading content.
    listing01_test.go:19:       When checking "http://www.baidu.com" for status code "200"
    listing01_test.go:26:               Should be able to make the Get call. ✓
    listing01_test.go:31:               Should receive a "200" status.--- PASS: TestDownload (0.15s)
PASS
ok      grpcProject     0.393s

代码说明

一个测试函数 必须是公开的函数,并且以 Test 单词开头。不但函数名字要以 Test 开头,而且函数的签名必 须接收一个指向 testing.T 类型的指针,并且不返回任何值。如果没有遵守这些约定,测试框 架就不会认为这个函数是一个测试函数,也不会让测试工具去执行它。

指向testing.T类型的指针很重要。这个指针提供的机制可以报告每个测试的输出和状态。测试的输出格式没有标准要求。

使用方法 t.Log 来输出测试 的消息。这个方法还有一个名为 t.Logf 的版本,可以格式化消息**。如果执行 go test 的时候 没有加入冗余选项(-v),除非测试失败,否则我们是看不到任何测试输出的。**

每个测试函数都应该通过解释这个测试的给定要求(given need),来说明为什么应该存在这 个测试。对这个例子来说,给定要求是测试能否成功下载数据。在声明了测试的给定要求后,测 试应该说明被测试的代码应该在什么情况下被执行,以及如何执行。

测试的输出很清 晰,能描述测试的目的,同时包含了足够的信息。我们知道具体是哪个单元测试被运行,测试通 过了,并且运行消耗的时间是 393毫秒。

1.2 表组测试

如果测试可以接受一组不同的输入并产生不同的输出的代码,那么应该使用表组测试的方法进行测试。

表组测试除了会有一组不同的输入值和期望结果之外,其余部分都很像基础单元测试。 测试会依次迭代不同的值,来运行要测试的代码。每次迭代的时候,都会检测返回的结果。这便于在一个函数里测试不同的输入值和条件。

这里还是介绍一下,goland自带的表组测试例子。

package exmaple

func AddNum(a, b int) int {
	return a + b
}

1.选中方法,点击鼠标右键,点击Generate(直接Alt+Insert也行)
Go语言学习笔记之 单元测试_第1张图片

2.选择Test for selection
Go语言学习笔记之 单元测试_第2张图片

Test for selection:为该函数生成测试。

Test for file:为该文件所有函数生成测试。

Test for package:为该包下所有函数生成测试。

3.在生成的单元测试函数中添加测试用例

package exmaple

import "testing"

func TestAddNum(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		// TODO: Add test cases.
		{
			name: "1 add 1 should be 2",
			args: args{
				a: 1,
				b: 1,
			},
			want: 2,
		},
		{
			name: "3 add 4 should be 7",
			args: args{
				a: 3,
				b: 4,
			},
			want: 7,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := AddNum(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("AddNum() = %v, want %v", got, tt.want)
			}
		})
	}
}

结果:

$ go test -v
=== RUN   TestAddNum
=== RUN   TestAddNum/1_add_1_should_be_2
=== RUN   TestAddNum/3_add_4_should_be_7
--- PASS: TestAddNum (0.00s)
    --- PASS: TestAddNum/1_add_1_should_be_2 (0.00s)
    --- PASS: TestAddNum/3_add_4_should_be_7 (0.00s)
PASS
ok      grpcProject/exmaple     0.228s

推荐用这种生成方式,手敲代码容易出错。

1.3 基准测试

基准测试(一种压力测试)是一种测试代码性能的方法。

想要测试解决同一问题的不同方案的性能,以及查看 哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的 CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用 基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。 让我们看一组基准测试的函数,找出将整数值转为字符串的最快方法。

package exmaple

import (
	"fmt"
	"testing"
)

// BenchmarkSprintf 对 fmt.Sprintf 函数进行基准测试
func BenchmarkSprintf(b *testing.B) {
	number := 10

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		fmt.Sprintf("%d", number)
	}
}

基准测试函数必须以 Benchmark 开头,接受一个指向 testing.B 类型的指针作为唯一参数。 为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,所以这里使用了 for 循环。for 循环展示了如何使用 b.N 的值。

基准测试框架默认会在持续 1 秒的时间内,反复调用需要测试的函数。测试框架每次调用测试函数时,都会增加 b.N 的值。第一次调用时,b.N 的值为 1。需要注意,一定要将所有要进 行基准测试的代码都放到循环里,并且循环要使用 b.N 的值。否则,测试的结果是不可靠的。

如果我们只希望运行基准测试函数,需要加入-bench 选项

go test -v -run="none" -bench="BenchmarkSprintf"

在这次 go test 调用里,我们给-run 选项传递了字符串"none",来保证在运行制订的基 准测试函数之前没有单元测试会被运行。这两个选项都可以接受正则表达式,来决定需要运行哪 些测试。由于例子里没有单元测试函数的名字中有 none,所以使用 none 可以排除所有的单元 测试。

查看一下输出:

$ go test -v -run="none" -bench="BenchmarkSprintf"
goos: windows
goarch: amd64
pkg: grpcProject/exmaple
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSprintf
BenchmarkSprintf-12     18444739                64.70 ns/op
PASS
ok      grpcProject/exmaple     1.503s

这个输出一开始明确了没有单元测试被运行,之后开始运行 BenchmarkSprintf 基准测 试。在输出 PASS 之后,可以看到运行这个基准测试函数的结果。第一个数字 18444739表示在 循环中的代码被执行的次数。在这个例子里,一共执行了 1844万次。之后的数字表示代码的性能, 单位为每次操作消耗的纳秒(ns)数。这个数字展示了这次测试,使用 Sprintf 函数平均每次 花费了 64 纳秒。

最后,运行基准测试输出了 ok,表明基准测试正常结束。之后显示的是被执行的代码文件的名字。 最后,输出运行基准测试总共消耗的时间。默认情况下,基准测试的最小运行时间是 1 秒。你会看到 这个测试框架持续运行了大约 1.5 秒。如果想让运行时间更长,可以使用另一个名为-benchtime 的 选项来更改测试执行的最短时间。让我们再次运行这个测试,这次持续执行 3 秒。

$ go test -v -run="none" -bench="BenchmarkSprintf" -benchtime="3s"
goos: windows
goarch: amd64
pkg: grpcProject/exmaple
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSprintf
BenchmarkSprintf-12     56794875                63.31 ns/op
PASS
ok      grpcProject/exmaple     3.901s

这次 Sprintf 函数运行了 5679万次,持续了 3.901 秒。这个函数的执行性能并没有太大的 变化,这次的性能是每次操作消耗 63 纳秒。有时候,增加基准测试的时间,会得到更加精确的 性能结果。对大多数测试来说,超过 3 秒的基准测试并不会改变测试的精确度。只是每次基准测试的结果会稍有不同。

运行基准测试时,另一个很有用的选项是-benchmem 选项。这个选项可以提供每次操作分 配内存的次数,以及总共分配内存的字节数。

$ go test -v -run="none" -bench="BenchmarkSprintf" -benchtime="3s" -benchmem
goos: windows
goarch: amd64
pkg: grpcProject/exmaple
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSprintf
BenchmarkSprintf-12     56487511                63.43 ns/op            2 B/op          1 allocs/op
PASS
ok      grpcProject/exmaple     3.879s

这次输出的结果会多出两组新的数值:一组数值的单位是 B/op,另一组的单位是 allocs/op。

单位为 allocs/op 的值表示每次操作从堆上分配内存的次数。

单位为 B/op 的值表示每次操作分配的字节数。

你可能感兴趣的:(GO语言学习,单元测试,数据库)