前言

零基础小白可放心食用!有很多概念解释比如匹配or通配符等等,一起加油!

学习来源:

Go 的单元测试是 Go 语言中内置的一个重要功能,支持开发者快速验证代码的正确性。通过标准库 testing 和相关工具(如 go test),你可以编写简洁、高效的单元测试用例。

单元测试基本概念

  • 单元测试是指对代码中最小的可测试部分(如函数或方法)进行验证,以确保其行为符合预期。
  • 在 Go 中,单元测试文件以 _test.go 为后缀,测试函数以 Test 开头。
  • Go 的测试框架是内置的,运行 go test 命令即可执行测试。

一个简单例子

Go 语言推荐测试文件和源代码文件放在一块,测试文件以 _test.go 结尾。比如,当前 package 有 calc.go 一个文件,我们想测试 calc.go 中的 Add 和 Mul 函数,那么应该新建 calc_test.go 作为测试文件。

1
2
3
example/
|--calc.go
|--calc_test.go

例如calc.go代码如下
1
2
3
4
5
6
7
8
9
package main

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

func Mul(a int, b int) int {
return a * b
}

则在同一个包中的calc_test.go可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "testing"

func TestAdd(t *testing.T) {
if ans := Add(1, 2); ans != 3 {
t.Errorf("1 + 2 expected be 3, but %d got", ans)
}

if ans := Add(-10, -20); ans != -30 {
t.Errorf("-10 + -20 expected be -30, but %d got", ans)
}
}

  • 文件名必须以 _test.go 结尾。
  • 测试函数:
    • 名称必须以 Test 开头,例如 TestAdd。
    • 参数为 *testing.T 类型,用于报告测试失败和日志。
  • 测试文件和函数需要在同一个包中。

*testing.T 是用于单元测试的参数类型,表示测试上下文。它提供了一系列方法,用于报告测试状态、记录日志、跳过测试等。

testing.B 是基准测试的参数类型,表示基准测试上下文。基准测试用于衡量代码的性能,通过* testing.B 提供的方法控制基准测试的运行。*

*testing.M 是 TestMain 函数的参数类型,用于控制测试的整个生命周期。TestMain 是所有测试的入口点,可以在运行测试前后进行全局的初始化和清理工作。

运行go test,改package下所有测试都会被运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ go test
ok example 0.009s
```

或 go test -v,-v 参数会显示每个用例的测试结果,另外 -cover 参数可以查看覆盖率。

```bash
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestMul
--- PASS: TestMul (0.00s)
PASS
ok example 0.007s

覆盖率
衡量测试用例对代码检查程度的指标,表示被测试的代码中实际被执行的部分占整个代码的比例。Go 提供了内置工具来分析测试覆盖率,通过 -cover 参数可以快速查看。
覆盖率通常以百分比表示,用来评估测试的有效性。测试覆盖率有以下几种类型:

  • 语句覆盖率(Statement Coverage):被执行的代码语句占总语句数的比例。
  • 分支覆盖率(Branch Coverage):被测试的代码中,分支条件(如 if 和 else)被覆盖的比例。
  • 函数覆盖率(Function Coverage):被调用的函数占总函数数的比例。

在 Go 中,-cover 参数默认显示语句覆盖率。

如果只想运行其中的一个用例,例如 TestAdd,可以用 -run 参数指定,该参数支持通配符 *,和部分正则表达式,例如 ^、$。

1
2
3
4
5
$ go test -run TestAdd -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok example 0.007s

匹配,通配符与正则表达式

  • 匹配:在 Go 的单元测试中,-run 参数用于指定运行哪些测试用例,通过匹配测试函数的名称来决定哪些测试会被执行。具体来说,根据你提供的通配符或正则表达式,与测试函数的名字进行对比。如果函数的名字符合你给定的模式,那么该测试用例会被运行。
  • 匹配的种类:

    • 运行指定测试
      运行TestAddgo test -run=TestAdd

    • 使用通配符

      • 运行所有以Test开头的测试go test -run=Test*
    • 使用正则表达式
      运行以 Test 开头且包含 Add 或 Subtract 的测试函数go test -run=^Test(Add|Subtract)$

子测试

在 Go 中,子测试是一种强大的功能,可以在一个测试函数中组织和运行多个相关的子测试。通过使用子测试,你可以对一组逻辑相关的测试进行分组,并更清晰地描述每个测试的意图。这种功能通常用在参数化测试或者复杂场景的分层测试中。

基本概念

子测试是通过 t.Run(name, func(t *testing.T)) 创建的:

  • name:为子测试指定一个描述性名称,用于标识这个测试。
  • 匿名函数 func(t *testing.T):定义子测试的逻辑。
  • 子测试会在父测试中运行,可以嵌套多层,形成类似层级结构的测试组织方式。

用法

参数化测试

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
32
33
34
35
36
37
38
39
40
41
package main

import (
"testing"
)

// 被测试函数,应该在同源的另一个文件里
func Add(a, b int) int {
return a + b
}

// 测试函数
func TestAdd(t *testing.T) {
// 测试用例
tests := []struct {
//结构体切片tests,定义了一组测试输入和期望输出

name string
a, b int
expected int
}{
//子测试的名字会出现,更容易定位错误
{"Add positives", 1, 2, 3},
{"Add negatives", -1, -1, -2},
{"Add mixed", -1, 1, 0},
}

// 遍历测试用例并运行子测试
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//匿名函数,内联在t.Run中
//每个子测试调用时,匿名函数被立即执行
//匿名函数内部代码定义了子测试的具体行为
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
//t.Errorf 用于记录一条格式化的错误信息,同时标记当前测试失败。
}
})
}
}

运行go test -v

1
2
3
4
5
6
7
8
9
10
=== RUN   TestAdd
=== RUN TestAdd/Addnec
=== RUN TestAdd/Addpos
=== RUN TestAdd/Addmix
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/Addnec (0.00s)
--- PASS: TestAdd/Addpos (0.00s)
--- PASS: TestAdd/Addmix (0.00s)
PASS
ok go-practice 0.360s

嵌套子测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestMath(t *testing.T) {
// 一级子测试
t.Run("Addition", func(t *testing.T) {
if Add(1, 2) != 3 {
t.Error("Addition failed")
}
})

// 二级子测试
t.Run("Multiplication", func(t *testing.T) {
t.Run("Positive numbers", func(t *testing.T) {
if 2*3 != 6 {
t.Error("Multiplication failed for positive numbers")
}
})

t.Run("Negative numbers", func(t *testing.T) {
if -2*-3 != 6 {
t.Error("Multiplication failed for negative numbers")
}
})
})
}

帮助函数

Go 的 testing 包提供了一个特殊的函数 t.Helper(),可以用来标记一个函数是测试的帮助函数,从而在测试失败时,错误信息中忽略这个帮助函数的调用栈,直接指向调用帮助函数的实际测试函数。

无帮助函数示例

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

import "testing"

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

func TestAdd(t *testing.T) {
if result := Add(2, 3); result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
if result := Add(0, 0); result != 0 {
t.Errorf("Add(0, 0) = %d; want 0", result)
}
if result := Add(-1, -1); result != -2 {
t.Errorf("Add(-1, -1) = %d; want -2", result)
}
}

含有帮助函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
func checkAdd(t *testing.T, a, b, expected int) {
t.Helper() // 标记为帮助函数
if result := Add(a, b); result != expected {
t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, expected)
}
}

func TestAdd(t *testing.T) {
//复用,更简洁高效
checkAdd(t, 2, 3, 5)
checkAdd(t, 0, 0, 0)
checkAdd(t, -1, -1, -2)
}

setup和teardown

在 Go 的测试中,Setup 和 Teardown 是两个常见的概念,用于在测试开始之前执行初始化工作(Setup),以及在测试结束之后进行清理工作(Teardown)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func setup() {
fmt.Println("Before all tests")
}

func teardown() {
fmt.Println("After all tests")
}

func Test1(t *testing.T) {
fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
  • 集中控制测试环境:
    TestMain 允许在单个位置控制测试的初始化和清理逻辑,而无需在每个测试中手动处理。
  • 确保环境一致性:
    使用 setup 和 teardown 可以确保所有测试运行在一个受控的环境中,避免因测试环境问题导致的不一致结果。
  • 可扩展性:
    如果需要增加更多的全局操作(如日志记录或配置加载),可以很方便地在 setup 和 teardown 中添加。

基准测试

Go 中的基准测试(Benchmarking)用于评估代码的性能,帮助开发者了解程序在执行时的效率和优化空间。与常规的单元测试不同,基准测试主要关注执行时间和性能瓶颈的定位,通过 testing 包中的 testing.B 类型来实现。testing.B 提供了一些方法和属性,可以用于测量函数的执行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"testing"
)

// 被测试的函数
func Add(a, b int) int {
return a + b
}

// 基准测试函数
func BenchmarkAdd(b *testing.B) {
// 基准测试循环
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
  • BenchmarkAdd 是基准测试函数。基准测试函数必须以 Benchmark 开头,并接收一个 *testing.B 类型的参数。
  • b.N 是基准测试的循环次数。testing.B 会根据需要调整这个值,以确保测试的执行时间足够长,能准确地测量性能。
  • b.N 会自动增长,确保函数执行足够多次,以消除偶然的波动。
1
2
3
4
5
6
7
8
PS E:\code2024\go-practice> go test -bench .
goos: windows
goarch: amd64
pkg: go-practice
cpu: Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
BenchmarkAdd-8 1000000000 0.3078 ns/op
PASS
ok go-practice 0.829s

如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器,例如:

1
2
3
4
5
6
7
func BenchmarkHello(b *testing.B) {
... // 耗时操作
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}