Go单元测试

Go 语言中,单元测试是通过标准库中的 testing 包来实现的,该包提供了一组功能,使得编写、运行和管理单元测试变得简单和高效。

一、规则

  • 测试文件的命名规则

    Go 中的测试文件命名规则是在被测试的源文件名后面加上 _test.go。例如,如果你有一个 calculator.go 文件,相应的测试文件应该是 calculator_test.go

  • 测试函数的命名规则

    测试函数必须以 Test 开头,后面可以跟任何非空字符串,例如 TestAddTestSubtract 等。

  • 使用 testing.T 进行断言和错误报告

    在测试函数中,使用 testing.T 类型的参数来管理测试状态和输出。你可以使用 t.Error*t.Fail* 等方法来指示测试失败,并输出相关的错误信息。

二、单元测试示例

2.1 单个测试用例

在calculator包中定义了一个calculator函数,具体实现如下:

go 复制代码
package calculator

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

在当前目录下,我们创建一个calculator_test.go的测试文件,并定义一个测试函数如下:

go 复制代码
package calculator

import "testing"


func TestAdd(t *testing.T) {
	result := Add(1, 2)
	expected := 3
	if result != expected {
		t.Errorf("Add(1,2) return %d, expected %d", result, expected)
	}
}

要运行这个单元测试,可以使用 go test 命令。在命令行中进入到包含 calculator.go 和 calculator_test.go 的目录,然后执行go test,这些结果如下

go 复制代码
go test
PASS
ok      modu    0.226s

2.2 多个测试用例

在calculator_test.go中添加如下测试函数:

go 复制代码
func TestAdd2(t *testing.T) {
	result := Add(3, 2)
	expected := 3
	if result != expected {
		t.Errorf("Add(1,2) return %d, expected %d", result, expected)
	}
}

为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为go test -v参数,让它输出完整的测试结果。

go 复制代码
go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestAdd2
    calculator_test.go:17: Add(1,2) return 5, expected 3
--- FAIL: TestAdd2 (0.00s)
FAIL
exit status 1
FAIL    modu    0.216s

2.3 指定运行测试用例

go test -run 命令可以按照指定的模式运行测试。这个命令支持通过正则表达式来选择要运行的测试函数。

例如修正好TestAdd2用例之后,通过go tes -run=Add2只运行TestAdd2这个测试用例,结果是

go 复制代码
go test -run=Add2 -v
=== RUN   TestAdd2
--- PASS: TestAdd2 (0.00s)
PASS
ok      modu    0.198s

4. 跳过某些测试用例

新加测试函数

go 复制代码
func TestAdd3(t *testing.T) {
	if testing.Short() {
        t.Skip("short模式下会跳过该测试用例")
    }
	
	result := Add(3, 2)
	expected := 5
	if result != expected {
		t.Errorf("Add(1,2) return %d, expected %d", result, expected)
	}
}

当执行go test -short时,就会跳过testing.Short()标记的测试用例,结果是

go 复制代码
go test -short -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestAdd2
--- PASS: TestAdd2 (0.00s)
=== RUN   TestAdd3
    calculator_test.go:23: short模式下会跳过该测试用例
--- SKIP: TestAdd3 (0.00s)
PASS
ok      modu    0.635s

三、测试组和子测试

3.1 测试组和子测试

通过测试组和子测试,可以更友好来添加更多的测试用例,以及查看结果

go 复制代码
func TestAdd(t *testing.T) {
	tests := []struct {
		name     string
		x, y     int
		expected int
	}{
		{"Add1", 1, 2, 3},
		{"Add2", 3, 3, 6},
		{"Add3", 4, 5, 8},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := Add(tc.x, tc.y)
			if result != tc.expected {
				t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
			}
		})
	}
}

运行go test -v,结果是

go 复制代码
go test -v
=== RUN   TestAdd
=== RUN   TestAdd/Add2
=== RUN   TestAdd/Add3
    calculator_test.go:51: Add(4, 5) returned 9, expected 8
--- FAIL: TestAdd (0.00s)
    --- PASS: TestAdd/Add1 (0.00s)
    --- PASS: TestAdd/Add2 (0.00s)
    --- FAIL: TestAdd/Add3 (0.00s)
FAIL
exit status 1
FAIL    modu    0.190s

3.2 并行测试

Go语言天生支持并发,所以通过添加t.Parallel()来实现驱动测试并行化。

go 复制代码
func TestAdd(t *testing.T) {
	t.Parallel()  // 将 TLog 标记为能够与其他测试并行运行
	// 这里使用匿名结构体定义了若干个测试用例
	// 并且为每个测试用例设置了一个名称
	tests := []struct {
		name     string
		x, y     int
		expected int
	}{
		{"Add1", 1, 2, 3},
		{"Add2", 3, 3, 6},
		{"Add3", 4, 5, 8},
	}

	for _, tc := range tests {
		tc := tc  // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()  // 将每个测试用例标记为能够彼此并行运行
			result := Add(tc.x, tc.y)
			if result != tc.expected {
				t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
			}
		})
	}
}

3.3 测试覆盖率

使用go test -cover来查看测试覆盖率

go 复制代码
go test -cover
PASS
        modu    coverage: 100.0% of statements
ok      modu    1.149s

四、Go单元测试工具包 -- testify

在进行Go语言单元测试时,由于官方并未内置断言功能,我们通常需要使用大量的if...else...语句来校验测试结果。然而,通过使用第三方库如testify/assert,我们可以轻松地调用多种常用的断言函数,这些函数不仅能够简化测试代码,还能生成清晰易懂的错误描述信息,帮助我们快速定位问题。

在上面例子当中,我们使用if...else...语句来校验测试结果

go 复制代码
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := Add(tc.x, tc.y)
			if result != tc.expected {
				t.Errorf("Add(%d, %d) returned %d, expected %d", tc.x, tc.y, result, tc.expected)
			}
		})
	}

现在可使用testify/assert之将上述判断过程简化如下:

go 复制代码
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			result := Add(tc.x, tc.y)
			assert.Equal(t, result, tc.expected)
		})
	}

testify/require拥有testify/assert所有断言函数,它们的唯一区别就是testify/require遇到失败的用例会立即终止本次测试。

相关推荐
夜色呦3 小时前
深入MOJO编程语言的单元测试世界
单元测试·mojo
五敷有你3 小时前
Go:hello world
开发语言·后端·golang
拔剑纵狂歌4 小时前
Golang异常处理机制
开发语言·后端·golang·go
clisk5 小时前
GO语言入门之准备
开发语言·后端·golang
刘铸纬16 小时前
Golang中defer和return顺序
开发语言·后端·golang
liupenglove1 天前
golang线程池ants-实现架构
开发语言·后端·golang·多线程
菜鸟开卷1 天前
Go 语言入门(一)
开发语言·后端·golang
__AtYou__1 天前
Golang | Leetcode Golang题解之第220题存在重复元素III
leetcode·golang·题解
stormsha1 天前
Go 依赖注入设计模式
开发语言·设计模式·golang
IT云清1 天前
Apache Seata分布式Go Server正式开源-TaaS设计简介
分布式·golang·apache