Skip to content

8.1 测试技巧:单元测试(Unit Test)

单元测试(Unit Test, UT) 是健康项目里很重要的一环:需求变动多、多人协作时,没有测试兜底,很容易在「改 A 坏 B」时把问题带到线上。

写测试短期多写一点代码,长期却能减少回归 bug、方便重构。不少团队排期紧,测试被挤掉——一旦核心逻辑缺少用例,后面补的成本会更高。

典型痛点包括:

  • 同事改了你不熟的函数,漏测异常分支,问题直接上线。
  • 函数分支多、改动频繁,每次手工验证场景费时费力。

一、如何写单元测试

1. 准备示例代码

在空目录中初始化模块(模块路径可按项目修改):

bash
go mod init example.com/reverse

待测逻辑一般放在独立包里(如 package reverse),与测试文件并列,便于复用与 go test。若项目只是可执行程序,把代码写在 package main 里、同目录放 *_test.go 也同样合法,本章从常见库结构讲起。

新建 reverse.go,只放待测函数(包名与目录习惯一致,便于阅读):

go
package reverse

func Reverse(s string) string {
	b := []byte(s)
	for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
		b[i], b[j] = b[j], b[i]
	}
	return string(b)
}

2. 测试文件约定

Reverse 编写 reverse_test.go,与 reverse.go 同属 package reverse 即可(白盒,可直接测未导出细节);若写成 package reverse_test,则只能测导出 API,相当于黑盒测试,此处从简。

约定简要记:

约定说明
文件名*_test.go
函数名TestXxxXxx 首字母大写
参数必须是 t *testing.T
import "testing"

3. 快速入门:第一个测试

先写只测一组数据的测试,把「导入 testing → 调用被测函数 → 与期望比较 → 失败则 t.Errorf」跑通。下面在 reverse_test.go 中编写:

go
package reverse

import "testing"

func TestReverseHello(t *testing.T) {
	in := "Hello, world"
	want := "dlrow ,olleH"
	got := Reverse(in)
	if got != want {
		t.Errorf("Reverse(%q) == %q, want %q", in, got, want)
	}
}

在模块根目录执行 go test / go test -v,应能通过;至此已具备被测实现与一个可执行的测试函数。

4. 核心测试方法

*testing.T 上常用的失败标记、日志与子测试入口如下(均通过参数 t 调用):

方法说明
t.Error() / t.Errorf()标记当前测试失败,继续执行当前测试函数内后续代码。
t.Fatal() / t.Fatalf()标记失败并立即终止当前测试函数(其后代码不再执行)。
t.Log() / t.Logf()输出日志;仅在使用 go test -v才会出现在测试输出中(无 -v 时默认不打印)。
t.Run()创建子测试并指定名称,便于分层组织用例及与 -run 的子名匹配;示例见本章第三节

5. 表驱动测试

多组输入/期望都要测时,把用例放进切片里循环执行,叫作表驱动(table-driven):结构清晰,加新用例只往表里加一行。

上文「快速入门:第一个测试」相比,核心仍是「调用 Reverse → 比较 → t.Errorf」,此处用 for 遍历多组 in / wantt.Errorf 不会中断整个测试函数,其余用例仍会执行(遇错即停可用 t.Fatalf,见上文「核心测试方法」)。

go
package reverse

import "testing"

func TestReverse(t *testing.T) {
	testcases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{" ", " "},
		{"!12345", "54321!"},
	}
	for _, tc := range testcases {
		rev := Reverse(tc.in)
		if rev != tc.want {
			t.Errorf("Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
		}
	}
}

同一文件里可同时保留 TestReverseHelloTestReverse;若只保留一种写法,删除不再需要的测试函数即可。

二、执行测试用例

包含 go.mod 的模块根目录(或对应包目录)执行:

1. 默认:go test

不加参数时,运行当前包下所有测试,通过则只打印简要结果

bash
go test
text
PASS
ok  	example.com/reverse	0.012s

ok 后为模块导入路径与耗时,以本机为准。)

2. 详细输出:go test -v

-v(verbose)会打印每个测试函数的执行情况(如 RUN / PASS):

bash
go test -v
text
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok  	example.com/reverse	0.012s

若某条断言失败,t.Errorf 会输出差异,且该测试记为失败;可用 -count=1 禁用缓存、反复确认(调试时常用)。

3. 常用命令示例

以下命令均在待测包所在目录(或模块上下文中针对该包的路径)下执行;go test 默认处理当前目录对应的那一个包

运行当前包内全部测试(逐条输出):

bash
go test -v

只运行名称匹配的测试函数-run 的值为正则;是否加 -v 均可,加 -v 便于查看日志):

bash
go test -v -run TestAdd

-run 使用正则匹配完整测试名(默认未锚定,子串即可命中;需精确到某一函数时可写更严的正则)。子测试名形如 TestFoo/bar 时的写法见第三节

查看测试覆盖率:

bash
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

-cover 在终端打印当前包的语句覆盖率摘要;-coverprofile 将覆盖率数据写入指定文件(文件名可自定),再由 go tool cover -html 生成本地 HTML 报告并在浏览器中查看。go tool covergo test 一样随 Go 安装提供。

三、子测试与 -run

1. 按名字过滤:-run

-run 的参数是正则,只运行函数名匹配的测试。例如只跑 TestReverse(与第二节第 3 小节命令形式相同):

bash
go test -v -run TestReverse

输出与「跑全部测试」时类似,只是只执行匹配的测试函数。

2. 子测试:t.Run

复杂场景可把一个大测例拆成多个 子测试,每个子测试有自己的名字,便于单独跑、单独看日志:

go
package reverse

import "testing"

func TestReverse(t *testing.T) {
	t.Run("foo", func(t *testing.T) {
		testcases := []struct {
			in, want string
		}{
			{"Hello, foo", "oof ,olleH"},
		}
		for _, tc := range testcases {
			rev := Reverse(tc.in)
			if rev != tc.want {
				t.Errorf("[foo] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
			}
		}
	})

	t.Run("bar", func(t *testing.T) {
		testcases := []struct {
			in, want string
		}{
			{"Hello, bar", "rab ,olleH"},
		}
		for _, tc := range testcases {
			rev := Reverse(tc.in)
			if rev != tc.want {
				t.Errorf("[bar] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
			}
		}
	})

	t.Run("zh_ch", func(t *testing.T) {
		testcases := []struct {
			in, want string
		}{
			{"金山集团", "团集山金"},
			{"😀😃😄", "😀😃😄"},
		}
		for _, tc := range testcases {
			rev := Reverse(tc.in)
			if rev != tc.want {
				t.Errorf("[zh_ch] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
			}
		}
	})
}

子测试全名是 主测试名/子名。只跑 foo 子测试可写:

bash
go test -v -run TestReverse/foo

典型输出类似:

text
=== RUN   TestReverse
=== RUN   TestReverse/foo
--- PASS: TestReverse/foo (0.00s)
--- PASS: TestReverse (0.00s)
PASS
ok  	example.com/reverse	0.012s

只跑 bar-run TestReverse/bar。需要匹配层级时,正则要能对上完整名字(细节请用 go help testflag 在本地查阅)。

四、性能测试:基准测试(Benchmark)

日常说的「压力测试」在 Go 里常通过 testing 包的基准测试完成:反复调用某段代码,由运行环境统计耗时、分配次数等,用来观察热点、对比优化前后差异。它与单元测试共用 go test,但默认不会跑基准函数,需要加 -bench 参数。

1. 函数格式

  • 函数名以 Benchmark 开头,且 Benchmark 后首字母大写(与 Test 规则类似)。
  • 参数为 b *testing.B,循环里写 for i := 0; i < b.N; i++:框架会调整 b.N,使单次基准运行时间足够稳定。
go
package reverse

import "testing"

func BenchmarkReverse(b *testing.B) {
	s := "Hello, world"
	for i := 0; i < b.N; i++ {
		Reverse(s)
	}
}

若基准前有一次性初始化(读文件、建连接等),可用 b.StopTimer() / b.StartTimer() 包住初始化,避免把准备时间算进被测函数。

2. 如何运行

在包目录执行(示例:只跑名字里含 Reverse 的基准):

bash
go test -bench=Reverse

跑当前包全部基准可写 -bench=.. 表示匹配任意)。需要同时看内存分配时可加 -benchmem

示意输出(数值随机器变化):

text
goos: darwin
goarch: amd64
pkg: example.com/reverse
BenchmarkReverse-8   	50000000	        24.1 ns/op
PASS
ok  	example.com/reverse	0.856s

-8 表示 GOMAXPROCSns/op 表示每次 b.N 循环里执行一次循环体的平均耗时(纳秒/次)。加 -benchmem 时还会多出 B/opallocs/op 等列。

五、机制与约定速查

以下条目可与第一节至第四节中的示例对照;完整标志与行为以 go help testgo help testflagtesting 包文档为准。

1. go test 与执行流程

go test 随 Go 发行版提供:在包内扫描 *_test.go 中符合约定的函数,生成临时测试入口程序,编译、运行并汇总通过/失败,再清理中间产物。测试源文件使用与普通 Go 代码相同的语法与类型系统。

识别哪些函数参与测试、如何匹配 -run / -bench 等,由工具与 testing 包共同约定;实现细节可能随版本调整,以官方说明为准。

2. *_test.go 与正式发布构建

  • 与实现同目录的 *.go / *_test.go 便于就近维护测试。
  • go buildgo install 默认*_test.go 编入可执行文件或库产物;这些文件主要在执行 go test(及少数相关构建路径)时参与编译。

3. 测试相关函数前缀

同一份 *_test.go 中,以下前缀的函数由 go test 按规则调度(是否运行还依赖是否传入 -bench 等参数):

前缀典型签名作用
Testfunc TestXxx(t *testing.T)单元测试:断言行为是否符合预期
Benchmarkfunc BenchmarkXxx(b *testing.B)基准测试:性能与分配(需 -bench
Examplefunc ExampleXxx(),可选 // Output:示例:文档与可运行样例,输出可由 go test 校验

Test*Benchmark* 已在前文示例中出现;Example* 本文未展开。

4. Test* 函数约定(编译与识别)

  1. 源文件以 _test.go 结尾。
  2. 函数名以 Test 开头,且 Test 后首字符为大写字母(如 TestReverse,不能为 Testreverse)。
  3. 签名为 func TestXxx(t *testing.T),文件内 import "testing"
  4. 在待测包所在目录(或模块上下文中指向该包)执行 go test 以运行该包测试。

5. 常用命令行标志

标志含义(简述)
-v逐条输出测试的运行与结果(见第二节
-run regexp仅运行名称匹配正则的测试(见第三节,含 TestFoo/bar 形式)
-count n每个测试执行 n 次;-count=1 常用于绕过测试结果缓存以便调试
-cover语句覆盖率摘要(-coverprofilego tool cover第二节第 3 小节
-bench regexp运行基准测试(见第四节);匹配 Benchmark* 名称
-benchmem-bench 联用时可输出分配相关列

go test 子命令、环境变量、覆盖率输出格式等有关的其余选项,以本地 go help test 为准。