golang--测试

一、单元测试 (Unit Testing)

shell 复制代码
wrl@DESKTOP-99LNSVB MINGW64 /d/goBackend/src/mainTest/benchmark (master)
$ ls
Benchmark_test.go  go.mod  main.go  split.go

1.1 基本概念

单元测试是对软件中的最小可测试单元进行检查和验证,在Go中通常指函数或方法。

1.2 测试文件命名规则

  • 测试文件必须以 _test.go 结尾
  • 测试文件与被测试文件在同一包内
  • 测试函数必须以 Test 开头

1.3 基本示例

被测试代码 (math.go):

go 复制代码
package math

// Add 返回两个整数的和
func Add(a, b int) int {
    return a + b
}

// Multiply 返回两个整数的积
func Multiply(a, b int) int {
    return a * b
}

测试代码 (math_test.go):

其中参数t用于报告测试失败和附加的日志信息。

go 复制代码
package math

import "testing"

// TestAdd 测试Add函数
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// TestMultiply 测试Multiply函数
func TestMultiply(t *testing.T) {
    result := Multiply(2, 3)
    expected := 6
    
    if result != expected {
        t.Errorf("Multiply(2, 3) = %d; want %d", result, expected)
    }
}

1.4 表格驱动测试

这是Go中常用的测试模式,可以批量测试多个用例:

go 复制代码
func TestAddTableDriven(t *testing.T) {
    testCases := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数相加", 2, 3, 5},
        {"负数相加", -2, -3, -5},
        {"零值相加", 0, 0, 0},
        {"正负相加", 5, -3, 2},
    }
    
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            result := Add(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tc.a, tc.b, result, tc.expected)
            }
        })
    }
}

1.5 测试辅助函数

go 复制代码
func assertEqual(t *testing.T, got, want int) {
    t.Helper()  // 标记为辅助函数,使错误报告更清晰
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestWithHelper(t *testing.T) {
    result := Add(1, 2)
    assertEqual(t, result, 3)
}

1.6 testing.T的拥有的方法如下

go 复制代码
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) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

  1. Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:
go 复制代码
wrl@DESKTOP-99LNSVB MINGW64 /d/goBackend/src/mainTest/benchmark (master)
$ go test -cover
PASS
coverage: 100.0% of statements
ok      mainTest/benchmark      0.909s
  1. 生成覆盖率数据文件
go 复制代码
# 生成覆盖率数据文件
go test -coverprofile=coverage.out

# 输出文件包含:
# 1. 每个文件的执行统计
# 2. 每行代码的执行次数
# mode: 覆盖模式(set, count, atomic)
# 文件名:起始行.起始列,结束行.结束列 语句数 执行次数

mode: set
mainTest/benchmark/main.go:3.14,3.15 0 0
mainTest/benchmark/split.go:5.45,8.13 2 1
mainTest/benchmark/split.go:8.13,12.3 3 1
mainTest/benchmark/split.go:13.2,14.8 2 1
  1. 查看详细覆盖率报告
go 复制代码
# 生成HTML格式的覆盖率报告,要先生成coverage.out文件,再使用这个命名,自动会打开html文件
go tool cover -html=coverage.out

# 生成文本报告
$ go tool cover -func=c.out
mainTest/benchmark/main.go:3:   main            0.0%
mainTest/benchmark/split.go:5:  Split           100.0%
total:                          (statements)    100.0%

# 按包查看覆盖率
go tool cover -func=coverage.out | grep "your/package"

二、基准测试 (Benchmark Testing)

2.1 基本概念

基准测试用于测量代码的性能,评估函数在不同输入下的执行时间和内存使用情况。

2.2 基准测试规则

  • 函数以 Benchmark 开头
  • 参数为 *testing.B
  • 使用 b.N 作为循环次数

2.3 基本示例

go 复制代码
// BenchmarkAdd 测试Add函数的性能
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(10, 20)
    }
}

// BenchmarkMultiply 测试Multiply函数的性能
func BenchmarkMultiply(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Multiply(10, 20)
    }
}

2.4 带参数的基准测试

go 复制代码
func BenchmarkAddDifferentInputs(b *testing.B) {
    testCases := []struct {
        name string
        a, b int
    }{
        {"小数字", 1, 2},
        {"中数字", 100, 200},
        {"大数字", 10000, 20000},
    }
    
    for _, tc := range testCases {
        b.Run(tc.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                Add(tc.a, tc.b)
            }
        })
    }
}

2.5 内存分配基准测试

go 复制代码
func BenchmarkStringConcatenation(b *testing.B) {
    b.ReportAllocs()  // 报告内存分配情况
    
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    b.ReportAllocs()
    
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        for j := 0; j < 100; j++ {
            builder.WriteString("a")
        }
        _ = builder.String()
    }
}

三、运行测试

3.1 常用命令

bash 复制代码
# 运行当前目录下所有测试
go test

# 显示详细信息
go test -v

# 运行特定测试函数
go test -v -run TestAdd

# 使用正则表达式匹配测试
go test -v -run "TestAdd.*"

# 运行基准测试
go test -bench=.

# 运行特定基准测试
go test -bench=BenchmarkAdd

# 基准测试并显示内存分配
go test -bench=. -benchmem

# 指定基准测试运行时间
go test -bench=. -benchtime=5s

# 并行运行测试
go test -parallel 4

# 生成覆盖率报告
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

3.2 并行测试

go 复制代码
func TestParallel(t *testing.T) {
    t.Parallel()  // 标记为可并行运行
    
    // 测试代码...
}

四、测试辅助工具

4.1 测试Main函数

go 复制代码
func TestMain(m *testing.M) {
    // 测试前的设置
    fmt.Println("开始测试...")
    
    // 运行所有测试
    code := m.Run()
    
    // 测试后的清理
    fmt.Println("测试结束...")
    
    // 退出
    os.Exit(code)
}

4.2 子测试

go 复制代码
package split

import "strings"

// split package with a single split function.

// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
	i := strings.Index(s, sep)

	for i > -1 {
		result = append(result, s[:i])
		s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}

如果测试用例比较多的时候,我们是没办法一眼看出来具体是哪个测试用例失败了。我们可能会想到下面的解决办法:

go 复制代码
func TestSplit(t *testing.T) {
	type test struct { // 定义test结构体
		input string
		sep   string
		want  []string
	}
	tests := map[string]test{ // 测试用例使用map存储
		"simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		"wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		"more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
		"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
	}
	for name, tc := range tests {
		got := Split(tc.input, tc.sep)
		if !reflect.DeepEqual(got, tc.want) {
			t.Errorf("name:%s expected:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
		}
	}
}

上面的做法是能够解决问题的。同时Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试:

go 复制代码
func TestSplit(t *testing.T) {
	type test struct { // 定义test结构体
		input string
		sep   string
		want  []string
	}
	tests := map[string]test{ // 测试用例使用map存储
		"simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		"wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		"more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
		"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
	}
	for name, tc := range tests {
		t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
			got := Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("expected:%#v, got:%#v", tc.want, got)
			}
		})
	}
}
go 复制代码
split $ go test -v
=== RUN   TestSplit
=== RUN   TestSplit/leading_sep
=== RUN   TestSplit/simple
=== RUN   TestSplit/wrong_sep
=== RUN   TestSplit/more_sep
--- FAIL: TestSplit (0.00s)
    --- FAIL: TestSplit/leading_sep (0.00s)
        split_test.go:83: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/wrong_sep (0.00s)
    --- PASS: TestSplit/more_sep (0.00s)
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

我们都知道可以通过-run=RegExp来指定运行的测试用例,还可以通过/来指定要运行的子测试用例,例如:go test -v -run=Split/simple只会运行simple对应的子测试用例。

五、实际示例

5.1 计算器示例

go 复制代码
// calculator.go
type Calculator struct{}

func (c *Calculator) Add(a, b float64) float64 {
    return a + b
}

func (c *Calculator) Subtract(a, b float64) float64 {
    return a - b
}
go 复制代码
// calculator_test.go
func TestCalculator(t *testing.T) {
    calc := &Calculator{}
    
    t.Run("加法", func(t *testing.T) {
        result := calc.Add(5.5, 2.5)
        expected := 8.0
        if result != expected {
            t.Errorf("Add(5.5, 2.5) = %f; want %f", result, expected)
        }
    })
    
    t.Run("减法", func(t *testing.T) {
        result := calc.Subtract(5.5, 2.5)
        expected := 3.0
        if result != expected {
            t.Errorf("Subtract(5.5, 2.5) = %f; want %f", result, expected)
        }
    })
}

func BenchmarkCalculatorAdd(b *testing.B) {
    calc := &Calculator{}
    for i := 0; i < b.N; i++ {
        calc.Add(float64(i), float64(i+1))
    }
}

六、最佳实践

  1. 测试命名: 使用清晰的测试名称,描述测试场景
  2. 表格驱动: 使用表格驱动测试组织多个测试用例
  3. 避免依赖: 测试不应该依赖外部环境
  4. 快速执行: 保持测试快速执行
  5. 独立测试: 每个测试应该独立,不依赖其他测试的状态
  6. 覆盖率: 追求合理的测试覆盖率,但不是100%

七、常用断言库

虽然Go标准库不提供断言,但社区有多个流行的测试库:

go 复制代码
// 使用testify库
import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestWithTestify(t *testing.T) {
    assert.Equal(t, 4, Add(2, 2), "它们应该相等")
    assert.NotNil(t, someValue)
    assert.Error(t, err)
}
相关推荐
linksinke3 小时前
在windows系统上搭建Golang多版本管理器(g)的配置环境
开发语言·windows·golang
卜锦元4 小时前
Golang后端性能优化手册(第二章:缓存策略与优化)
开发语言·数据库·后端·性能优化·golang
今夕资源网4 小时前
go-tcnat内网端口映射 端口穿透 GO语言 免费开源
开发语言·后端·golang·go语言·端口映射·内网端口映射
Tony Bai4 小时前
告别“If-Else”地狱:OpenFeature 如何重塑 Go 应用的特性开关管理?
开发语言·后端·golang
源代码•宸6 小时前
goframe框架签到系统项目开发(用户认证、基于 JWT 实现认证、携带access token获取用户信息)
服务器·开发语言·网络·分布式·后端·golang·jwt
思成Codes6 小时前
Gin路由:构建高效RESTful API
golang·restful·xcode·gin
Clarence Liu6 小时前
Go Map进化史:从桶链式哈希表到Swiss Table的源码级剖析
golang·哈希算法·散列表
卜锦元6 小时前
Golang后端性能优化手册(第一章:数据库性能优化)
大数据·开发语言·数据库·人工智能·后端·性能优化·golang
小高Baby@6 小时前
map的数据结构,扩容机制,key是无序的原因
数据结构·golang·哈希算法