Go Test

单元测试

单元测试函由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

分析原因:

  1. 在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) --- 判断 expectedactual 是否相等。

  • assert.NotEqual(t, expected, actual, message) --- 判断 expectedactual 是否不相等。

  • 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...

相关推荐
起飞的小鸟13 小时前
Go 中的加锁方式
go
快乐源泉13 小时前
【设计模式】观察者,只旁观?不,还可随之变化
后端·设计模式·go
Piper蛋窝16 小时前
Go 1.3 相比 Go1.2 有哪些值得注意的改动?
go
纪元A梦18 小时前
华为OD机试真题——天然蓄水库(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·c语言·javascript·c++·python·华为od·go
一个热爱生活的普通人20 小时前
浅谈池化思想:以 database/sql 连接池为例
后端·go
快乐源泉20 小时前
【设计模式】桥接,是设计模式?对,其实你用过
后端·设计模式·go
武斌20 小时前
go-doudou CLI命令行工具详解
后端·微服务·go
Piper蛋窝21 小时前
Go 1.2 相比 Go1.1 有哪些值得注意的改动?
后端·go
ling__wx1 天前
go学习记录(第一天)
学习·go
chxii1 天前
2.2goweb解析http请求信息
go