单元测试
单元测试函由testing
包和go test
命令组成,格式如下:
go
func TestName(t *testing.T){
// ...
}
参数t
用于报告测试失败和附加的日志信息。 testing.T
的拥有的方法如下:
go
func (c *T) Cleanup(func())
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() strin
假设我们有一个math.go
文件:
go
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
对应的测试文件math_test.go
:
go
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func TestSubtract(t *testing.T) {
result := Subtract(5, 2)
expected := 3
if result != expected {
t.Errorf("Subtract(5, 2) = %d; want %d", result, expected)
}
}
运行测试:
go
go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestSubtract
--- PASS: TestSubtract (0.00s)
PASS
ok base/testing/menu_testing 0.496s
基础命令
常用测试命令
go
# 运行所有测试
go test ./...
# 运行特定测试
go test -run TestName
# 显示详细输出
go test -v
# 并行运行测试
go test -parallel 4
# 跳过长时间运行的测试
go test -short
# 生成测试覆盖率HTML报告
go test -coverprofile=coverage.out && go tool cover -html=coverage.out
表驱动测试
适用于多个输入输出组合的测试,可以通过一个结构体数组或者切片定义一组测试用例,然后遍历这些用例进行测试,他的扩展能力强,新增测试只需要添加一条记录。
go
func Add(a, b int) int {
return a + b
}
表驱动测试
go
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正数相加", 1, 2, 3},
{"负数相加", -1, -2, -3},
{"正负相加", -1, 2, 1},
{"零相加", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
}
})
}
}
性能测试Benchmark
在Go中,Benchmark
是一种用于衡量代码性能的测试方法。通过基准测试,你可以了解某段代码执行的效率,并根据性能结果进行优化。Go 提供了内置的基准测试功能,可以帮助你测试函数的执行速度和资源消耗。
基本概念
-
Benchmark 测试 :用于测试函数或代码块的性能。通过运行代码多次并测量时间,
Benchmark
测试可以提供函数的执行时间和性能数据。 -
Benchmark 函数 :与普通的测试函数不同,
Benchmark
函数必须以Benchmark
开头,并接收一个*testing.B
类型的参数。
go
package main
import (
"testing"
)
// Fibonacci 函数
func Fibonacci(n int) int {
if n <= 0 {
return 0
} else if n == 1 {
return 1
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
// 基准测试 Fibonacci 函数
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(10) // 测试计算 Fibonacci(10) 的性能
}
}
go test -bench .
goos: windows
goarch: amd64
pkg: base/testing/testing
cpu: AMD Ryzen 7 8845H w/ Radeon 780M Graphics
BenchmarkFibonacci-16 c 204.5 ns/op
PASS
ok base/testing/testing 1.901s
-
BenchmarkFibonacci-12
:表示基准测试的名称以及 CPU 核心数(在这个例子中是 12)。 -
5757248
:表示基准测试被执行的次数。 -
204.5 ns/op
:表示每次调用Fibonacci(10)
的平均时间是 2.63 纳秒(ns)。 -
ns/op
:表示每次操作所消耗的时间(单位是纳秒)。这个数字越小,说明代码的性能越好。
基准测试常用命令行参数
参数 | 含义 | 示例 |
---|---|---|
-bench |
指定运行的基准测试函数 | -bench=. 运行所有基准测试 |
-benchtime |
指定每个基准测试运行的时间 | -benchtime=3s 每个测试运行 3 秒 |
-benchmem |
输出每次操作的内存分配信息 | -benchmem |
-cpu |
设置使用的 CPU 数量 | -cpu=1,2,4 分别在 1、2、4 核心下运行 |
-run |
搭配 -bench 使用时跳过普通测试 |
-run=^$ -bench=. 只运行基准测试 |
-count |
指定基准测试重复运行次数 | -count=5 运行每个基准测试 5 次 |
-timeout |
设置测试的最长时间(包含测试和基准测试) | -timeout=10s 最长允许测试 10 秒 |
注意事项
在进行基准测试前,需要加载大量数据的情况下,可以使用b.ResetTimer()
忽略加载数据的耗时,例如:
go
package main
import (
"testing"
"time"
)
// 假设我们有一个很耗时的准备函数:加载数据库、大文件等
func loadLargeDataset() []int {
time.Sleep(2 * time.Second) // 模拟耗时加载
data := make([]int, 10000)
for i := range data {
data[i] = i
}
return data
}
// 被测试的核心函数:对数据求和(我们只想测它的性能)
func process(data []int) int {
sum := 0
for _, v := range data {
sum += v
}
return sum
}
// 基准测试
func BenchmarkProcess(b *testing.B) {
// ⏳ 模拟耗时的准备工作
dataset := loadLargeDataset()
// ✅ 只想测试 process 函数,不包括上面的加载耗时
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(dataset)
}
}
模糊测试Fuzzing
Go 1.18在go工具链里引入了fuzzing模糊测试,可以帮助我们发现Go代码里的漏洞或者可能导致程序崩溃的输入。
fuzzing可以构造随机数据来找出代码里的漏洞或者可能导致程序崩溃的输入。通过fuzzing可以找出的漏洞包括SQL注入、缓冲区溢出、拒绝服务(Denial of Service)攻击和XSS(cross-site scripting)攻击等。
- Go模糊测试函数以FuzzXxx开头,单元测试函数以TestXxx开头
- Go模糊测试函数以
*testing.F
作为入参,单元测试函数以*testing.T
作为入参 - Go模糊测试会调用
f.Add
函数和f.Fuzz
函数。f.Add
函数把指定输入作为模糊测试的种子语料库(seed corpus),fuzzing基于种子语料库生成随机输入。f.Fuzz
函数接收一个fuzz target函数作为入参。fuzz target函数有多个参数,第一个参数是*testing.T
,其它参数是被模糊的类型(注意 :被模糊的类型目前只支持部分内置类型, 列在 Go Fuzzing docs,未来会支持更多的内置类型)。
以反转字符串函数为例,创建main.go文件
go
package fuzz
import "fmt"
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q\n", rev)
fmt.Printf("reversed again: %q\n", doubleRev)
}
为Reverse函数生成模糊测试:
go
// 模糊测试
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
// 运行结果
go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/13 completed
failure while testing seed corpus entry: FuzzReverse/8b6e98db2ed01837
fuzz: elapsed: 0s, gathering baseline coverage: 1/13 completed
--- FAIL: FuzzReverse (0.11s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:33: Number of runes: orig=1, rev=3, doubleRev=1
reverse_test.go:38: Reverse produced invalid UTF-8 string "\xab\xab\xe4"
FAIL
exit status 1
FAIL study/fuzz 0.283s
分析原因:
- 在testdata/fuzz/FuzzReverse中可以看见
string("䫫")
以中文为输入的数据,在Go语言中,字符串默认使用UTF-8编码,中文字符使用在UTF-8编码中需要三个字节表示,反转后的字节序列不符合UTF-8编码规范。
使用rune类型来处理Unicode字符:
go
func Reverse(s string) string {
b := []rune(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
在测试非法的Unicode字符时,反转得到的字符串不变,反转错误,需要对输入的字符串进行合法校验:
go
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
reverse_test.go
go
package fuzz
import (
"testing"
"unicode/utf8"
)
// 模糊测试
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
testify
testify
比较流行的Go语言测试库,核心内容:
- assert:断言
- mock:测试替身
- suite:测试套件
assert
-
assert.Equal(t, expected, actual, message)
--- 判断expected
和actual
是否相等。 -
assert.NotEqual(t, expected, actual, message)
--- 判断expected
和actual
是否不相等。 -
assert.Nil(t, object, message)
--- 判断对象是否为nil
。 -
assert.NotNil(t, object, message)
--- 判断对象是否不为nil
。 -
assert.True(t, condition, message)
--- 判断条件是否为true
。 -
assert.False(t, condition, message)
--- 判断条件是否为false
go
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
// 使用 assert 来断言 Add 函数的结果
assert.Equal(t, 4, Add(2, 2), "Expected 2 + 2 to equal 4")
}
Mock
testify
提供模拟功能,通常用于模拟接口,确保你的代码与外部依赖交互正确,例如,你在测试代码时需要使用数据层代码查数据库,可以使用Mock生成你期望的数据,不需要显式建立数据库连接。
go
package main
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// 定义一个接口
type Database interface {
Save(data string) error
}
// 创建一个 Mock 类型
type MockDatabase struct {
mock.Mock
}
// 实现接口方法
func (m *MockDatabase) Save(data string) error {
args := m.Called(data)
return args.Error(0)
}
// 被测试的函数
func SaveData(db Database, data string) error {
return db.Save(data)
}
func TestSaveData(t *testing.T) {
// 创建一个 Mock 对象
mockDB := new(MockDatabase)
// 设置期望:当 Save 被调用时,传入的参数是 "testdata",并返回 nil(表示没有错误)
mockDB.On("Save", "testdata").Return(nil)
// 调用被测试的函数
err := SaveData(mockDB, "testdata")
// 断言函数是否执行成功
assert.NoError(t, err)
// 验证所有期望都被调用
mockDB.AssertExpectations(t)
}
suite
可以创建一个结构化的测试环境,在多个测试方法之间共享初始化的数据或者状态。
基本方法和功能
-
suite.Suite
在使用
suite
时,首先需要定义一个结构体,这个结构体嵌入suite.Suite
类型。这种方式为你的测试套件提供了很多便利方法。 -
SetupTest()
在每个测试方法运行之前会调用
SetupTest()
,用于设置每个测试方法所需的初始状态或资源。 -
TearDownTest()
在每个测试方法运行之后会调用
TearDownTest()
,用于清理资源或进行必要的收尾工作。 -
SetupSuite()
在整个测试套件开始之前只会调用一次,用于初始化需要在所有测试方法之间共享的资源。
-
TearDownSuite()
在整个测试套件完成后只会调用一次,用于清理全局资源。
-
suite.Run(t, new(MyTestSuite))
使用
suite.Run
来运行定义好的测试套件。
go
package main
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 定义一个结构体,嵌入 `suite.Suite`
type MathTestSuite struct {
suite.Suite
}
// SetupSuite 在整个测试套件开始前执行一次
func (suite *MathTestSuite) SetupSuite() {
// 在这里做一些全局初始化工作
println("SetupSuite: Suite starts")
}
// TearDownSuite 在整个测试套件结束后执行一次
func (suite *MathTestSuite) TearDownSuite() {
// 在这里做一些全局清理工作
println("TearDownSuite: Suite ends")
}
// SetupTest 在每个测试方法开始前执行
func (suite *MathTestSuite) SetupTest() {
// 在这里做一些每个测试方法的初始化工作
println("SetupTest: Test starts")
}
// TearDownTest 在每个测试方法结束后执行
func (suite *MathTestSuite) TearDownTest() {
// 在这里做一些每个测试方法的清理工作
println("TearDownTest: Test ends")
}
// 测试方法
func (suite *MathTestSuite) TestAdd() {
result := 2 + 3
suite.Equal(5, result, "2 + 3 should equal 5")
}
func (suite *MathTestSuite) TestSubtract() {
result := 5 - 3
suite.Equal(2, result, "5 - 3 should equal 2")
}
// 使用 suite.Run 运行测试套件
func TestMathTestSuite(t *testing.T) {
suite.Run(t, new(MathTestSuite))
}
go test
SetupSuite: Suite starts
SetupTest: Test starts
TearDownTest: Test ends
SetupTest: Test starts
TearDownTest: Test ends
TearDownSuite: Suite ends
PASS
ok base/testing/testing 0.600s
参考链接
www.liwenzhou.com/posts/Go/un... github.com/jincheng9/g... geektutu.com/post/hpg-be...