4. Go测试
4.1. 单元测试
在 Go 语言中,单元测试通常使用 Go 自带的 testing 包进行
4.1.1. 规则
- 所有测试代码都以
_test.go结尾。 - 测试代码内部函数命名:
func TestXxxx(t *testing.T),即必须以Test开头,并且接收一个指向testing.T类型的指针作为参数。 - 初始化逻辑放到TestMain中
func TestMain(m *testing.M) {
// 测试前:数据装载、配置初始化等前置工作
code := m.Run()
// 测试后:释放资源等收尾工作
os.Exit(code)
}
4.1.2. 断言:assert
在单元测试中,assert 是用于验证代码输出是否符合预期的关键工具。它的作用是检查某个条件是否成立,如果条件不成立,则会抛出异常或显示错误消息,从而终止当前测试并标记为失败。
Go 语言的标准库中没有像其他语言(如 Python、Java)那样直接提供 assert 函数,但通过 testing 包中的 t.Errorf 或 t.Fatalf 来模拟断言的功能。此外,也有一些第三方库(如 Testify ) 提供了更丰富的断言功能。
标准库中的记录错误信息
Go 的 testing.T 提供了多种方法来执行断言,主要有:
t.Errorf:记录错误信息,但测试继续进行。t.Fatalf:记录错误信息并终止测试。
第三方库中的断言
为了提高可读性和简化代码,很多 Go 项目使用第三方库,如Testify,它提供了丰富的断言方法,使得测试代码更加简洁和直观。
- 安装 Testify
使用 go get 安装 Testify 库: go get github.com/stretchr/testify
- 使用 Testify
package math
// 导入
import (
"github.com/stretchr/testify/assert"
"testing"
)
// TestSum 使用 Testify 的断言库
func TestSum(t *testing.T) {
result := Sum(2, 3)
expected := 5
// 使用 assert.Equal 断言相等
assert.Equal(t, expected, result, "Sum(2, 3) should be 5")
}
常见的 Testify 断言方法
assert.Equal(t, expected, actual, msg):断言expected和actual是否相等。assert.NotEqual(t, expected, actual, msg):断言expected和actual是否不相等。assert.Nil(t, obj, msg):断言对象obj是否为nil。assert.NotNil(t, obj, msg):断言对象obj是否非nil。assert.True(t, condition, msg):断言条件是否为true。assert.False(t, condition, msg):断言条件是否为false。assert.Error(t, err, msg):断言err是否为错误。assert.NoError(t, err, msg):断言err是否没有错误。
4.1.3. 代码测试覆盖率
代码覆盖率是评估单元测试对代码的覆盖程度的一种指标。通过计算覆盖率,我们可以了解测试代码对项目源代码的覆盖情况,从而判断是否有未被测试的代码逻辑。
高覆盖率通常意味着更高的代码可靠性,但不一定意味着代码没有缺陷。
- 基本代码覆盖率命令
go test -cover
# 执行该命令后,会显示代码覆盖率的百分比,例如:
PASS
coverage: 85.0% of statements
ok yourmodule/path 0.002s
2. 生成覆盖率详细报告
go test -coverprofile=coverage.out
这会在当前目录下生成一个 coverage.out 文件,包含每个语句的覆盖情况。可以使用 go tool cover 来查看详细的 HTML 报告:
go tool cover -html=coverage.out
3. 生成覆盖率详细报告
Go 的覆盖率工具支持以下几种标记选项,可以分别统计函数、语句等不同粒度的覆盖率:
-
-covermode=count:统计代码被覆盖的次数。-covermode=set:仅记录是否覆盖,而不计数。-covermode=atomic:类似于count,但使用了原子计数操作,适合并发测试。
4.1.4. 简单测试示例
- 第一步:编写代码和测试代码文件
package hellotom
func HelloTom() string {
return "Hello Tom"
}
package hellotom
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expected := "Hello Tom"
if output != expected {
t.Errorf("Expected %s, but got %s", expected, output) // 标准库模拟断言
}
}
package hellotom
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHello(t *testing.T) {
output := HelloTom()
expected := "Hello Tom"
assert.Equal(t, output, expected) // 第三方库断言
}
- 第二步:运行测试
要运行测试,直接在所在目录下执行以下命令行命令:
go test
如果测试通过,输出将显示如下:
=== RUN TestHelloTom
--- PASS: TestHelloTom (0.00s)
PASS
ok github.com/Moonlight-Zhao/go-project-example/helloTom (cached)
如果测试失败,t.Errorf 会输出错误信息,帮助你定位问题。
Go 也提供了测试覆盖率功能,来检查你的测试覆盖了代码中的多少部分, 还可以生成详细的覆盖率报告。
go test -cover
PASS
coverage: 100.0% of statements
ok yourmodule/math 0.002s
4.2. MOCK测试
4.2.1. 基本概念
在单元测试中,Mock 测试(打桩测试) 用于模拟依赖组件的行为,以便在隔离的环境中测试代码的某一部分。Mock 对象可以替代真实的依赖组件,使测试更加灵活,并避免在测试中引入不可控的因素(如网络请求、数据库操作等)。
在 Go 中, 通常需要使用第三方库,比如 bou.ke/monkey 来实现 Monkey Patch。
4.2.2. monkey 使用示例
假设我们有一个函数 GetCurrentTime,它使用 time.Now() 返回当前时间。我们希望在测试中控制 time.Now() 的返回值,以便更精确地验证业务逻辑。
// timeutils.go
package timeutils
import (
"time"
)
func GetCurrentTime() time.Time {
return time.Now()
}
在实际测试时,直接调用 time.Now() 会产生动态数据,难以预测,因此我们可以使用 monkey 来替换 time.Now() 的实现。 如下所示:
package timeutils
import (
"testing"
"time"
"github.com/bouk/monkey"
)
func TestGetCureentTime(t *testing.T) {
// 设置固定的时间
fixedtime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
// 使用monkey代替time.Now()函数
patch := monkey.patch(time.Now, func() time.Time {
return fixedtime
})
defer patch.Unpatch() // 程序结束后,恢复time.Now()函数
// 调用GetCureentTime()函数
result := GetCurrentTime()
// 验证是否是固定时间
if result!= fixedtime {
t.Errorf("Expected %v, but got %v", fixedtime, result)
}
}
4.3. 基准测试
4.3.1. 基本概念
在 Go 中,基准测试(Benchmarking)是通过自带的 testing包 来进行性能评测的。基准测试的目的是测量函数的执行时间,从而了解代码在特定输入下的性能表现。
Go 的基准测试通常会包含以下几个部分:
- 基准测试函数:函数的签名是
func BenchmarkXxx(b *testing.B),其中Xxx是你想要测试的函数名,b是基准测试的对象。 - 测试逻辑:基准测试中通常会调用目标函数并重复执行,以测量它的平均执行时间。
b.N:Go 在执行基准测试时会多次调用你的函数,每次执行的次数由b.N决定。b.N是 Go 自动调整的,目的是让基准测试的时间足够长,以便得到准确的性能数据。
4.3.2. 测试示例
假设你有一个简单的函数,你想测试它的性能:
package mypackage
import (
"fmt"
)
// 需要进行基准测试的函数
func Add(a, b int) int {
return a + b
}
你可以为这个 Add 函数编写一个基准测试:
package mypackage
import (
"testing"
)
// 基准测试函数
func BenchmarkAdd(b *testing.B) {
// 在基准测试中,你应该避免每次都重新创建对象
// 使用 b.N 来控制循环的次数
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
运行基准测试:
在终端中,进入到包含测试文件的目录,执行以下命令:
go test -bench . # -bench . 命令会运行当前目录下所有以 Benchmark 开头的函数。
go test -bench BenchmarkAdd # 只想运行特定的基准测试函数
基准测试输出:
$ go test -bench .
goos: linux
goarch: amd64
pkg: mypackage
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAdd-12 1000000000 1.35 ns/op
PASS
ok mypackage 1.428s
BenchmarkAdd-12:表示基准测试的函数名和 CPU 核心数(-12表示 12 个 CPU 核心)。1000000000:表示运行的循环次数(即b.N的值)。1.35 ns/op:表示每次调用Add函数的平均耗时(单位:纳秒/次)。