• 快速掌握Golang单元测试与断言教程


    Go 在testing包中内置测试命令go test,提供了最小化但完整的测试体验。标准工具链还包括基准测试和基于代码覆盖的语句,类似于NCover(.NET)或Istanbul(Node.js)。本文详细讲解go编写单元测试的过程,包括性能测试及测试工具的使用,另外还介绍第三方断言库的使用。

    编写单元测试

    go中单元测试与语言中其他特性一样具有独特见解,如格式化、命名规范。语法有意避免使用断言,并将检查值和行为的责任留给开发人员。

    下面通过示例进行说明。我们编写Sum函数,实现数据求和功能:

    package main
    
    func Sum(x int, y int) int {
        return x + y
    }
    
    func main() {
        Sum(5, 5)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后在单独的文件中编写测试代码,测试文件可以在相同包中,或不同包中。测试代码如下:

    package main
    
    import "testing"
    
    func TestSum(t *testing.T) {
        total := Sum(5, 5)
        if total != 10 {
           t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Golang测试功能特性:

    • 仅需要一个参数,必须是t *testing.T
    • 以Test开头,接着单词或词组,采用骆驼命名法,举例:TestValidateClient
    • 调用t.Errort.Fail 表明失败(当然也可以使用t.Errorf提供更多细节)
    • t.Log用于提供非失败的debug信息输出
    • 测试文件必须命名为something_test.go ,举例: addition_test.go

    批量测试(test tables)

    test tables概念是一组(slice数组)测试输入、输出值:

    func TestSum(t *testing.T) {
    	tables := []struct {
    		x int
    		y int
    		n int
    	}{
    		{1, 1, 2},
    		{1, 2, 3},
    		{2, 2, 4},
    		{5, 2, 7},
    	}
    
    	for _, table := range tables {
    		total := Sum(table.x, table.y)
    		if total != table.n {
    			t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.x, table.y, total, table.n)
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如果需要触发错误,我们可以修改测试数据,或修改代码。这里修改代码return x*y, 输出如下:

    === RUN   TestSum
        math_test.go:61: Sum of (1+1) was incorrect, got: 1, want: 2.
        math_test.go:61: Sum of (1+2) was incorrect, got: 2, want: 3.
        math_test.go:61: Sum of (5+2) was incorrect, got: 10, want: 7.
    --- FAIL: TestSum (0.00s)
    
    FAIL
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    单元测试不仅要正向测试,更要进行负向测试。

    执行测试

    执行测试有两种方法:

    1. 在相同目录下运行命令:
    go test 
    
    • 1

    这会匹配任何packagename_test.go的任何文件。

    1. 使用完整的包名

    go test

    现在我们可以运行单元测试了,还可以增加参数go test -v获得更多输出结果。

    单元测试和集成测试的区别在于单元测试通常不依赖网络、磁盘等,仅测试一个功能,如函数。

    另外还可以查看测试语句覆盖率,增加-cover选项。但高覆盖率未必总是比低覆盖率好,关键是功能正确。
    如果执行下面命令,可以生成html文件,以可视化方式查看覆盖率:

    go test -cover -coverprofile=c.out
    go tool cover -html=c.out -o coverage.html 
    
    • 1
    • 2

    性能测试

    benchmark 测试衡量程序性能,可以比较不同实现差异,理解影响性能原因。

    go性能测试也有一定规范:

    • 性能测试函数名必须以Benchmark开头,之后大写字母或下划线。因此BenchmarkFunctionName()Benchmark_functionName()都是合法的,但Benchmarkfunctionname()不合法。这与单元测试以Test开头规则一致。

    虽然可以把单元测试和性能测试代码放在相同文件,但尽量避免,文件命名仍然以_test.go结尾。如单元测试文件为simple_test.go,性能测试为benchmark_test.go。

    下面通过示例进行说明,首先定义函数:

    func IsPalindrome(s string) bool {
    	for i := range s {
    		if s[i] != s[len(s)-1-i] {
    			return false
    		}
    	}
    	return true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    先编写单元测试,分别编写正向测试和负向测试:

    func TestPalindrome(t *testing.T) {
    	if !IsPalindrome("detartrated") {
    		t.Error(`IsPalindrome("detartrated") = false`)
    	}
    	if !IsPalindrome("kayak") {
    		t.Error(`IsPalindrome("kayak") = false`)
    	}
    }
    
    func TestNonPalindrome(t *testing.T) {
    	if IsPalindrome("palindrome") {
    		t.Error(`IsPalindrome("palindrome") = true`)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    接着编写基准测试(性能测试):

    func BenchmarkIsPalindrome(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		IsPalindrome("A man, a plan, a canal: Panama")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    执行性能测试

    go test -bench . -run notest
    
    • 1

    -bench参数执行所有性能测试,也可以使用正则代替. ,默认情况单元测试也会执行,因为单元测试种有错误,可以通过-run 参数指定值不匹配任何测试函数名称,从而仅执行性能测试。

    我们还可以指定其他参数,下面示例指定count为2,表示对现有测试执行两次分析。设置GOMAXPROCS为4,查看测试的内存情况,执行这些请求时间为2秒,而不是默认的1秒执行时间。命令如下:

    $ go test -bench=. -benchtime 2s -count 2 -benchmem -cpu 4 -run notest
    goos: windows
    goarch: amd64
    pkg: gin01/math
    cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
    BenchmarkIsPalindrome
    BenchmarkIsPalindrome-4         1000000000               1.349 ns/op           0 B/op          0 allocs/op
    BenchmarkIsPalindrome-4         1000000000               1.356 ns/op           0 B/op          0 allocs/op
    PASS
    ok      gin01/math      3.234s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • -4 : 执行测试的GOMAXPROCS数量

    • 1000000000 :为收集必要数据而运行的次数

    • 1.349 ns/op :测试每个循环执行速度

    • PASS:指示基准测试运行的结束状态。

    配置计算时间

    定义函数:

    func sortAndTotal(vals []int) (sorted []int, total int) {
    	sorted = make([]int, len(vals))
    	copy(sorted, vals)
    	sort.Ints(sorted)
    	for _, val := range sorted {
    		total += val
    		total++
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对应单元测试如下:

    func BenchmarkSort(b *testing.B) {
    	rand.Seed(time.Now().UnixNano())
    	size := 250
    	data := make([]int, size)
    	for i := 0; i < b.N; i++ {
    		for j := 0; j < size; j++ {
    			data[j] = rand.Int()
    		}
    		sortAndTotal(data)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    每次执行前,随机生成数组,造成性能测试不准确。

    为了更准确计算时间,可以使用下面函数进行控制:

    -StopTimer() : 停止计时器方法.
    -StartTimer() : 启动计时器方法.
    -ResetTimer() : 重置计时器方法.

    最终性能测试函数如下:

    func BenchmarkSort(b *testing.B) {
    	rand.Seed(time.Now().UnixNano())
    	size := 250
    	data := make([]int, size)
        // 开始前先重置
    	b.ResetTimer()
    	for i := 0; i < b.N; i++ {
            // 准备数据时停止计时
    		b.StopTimer()
    		for j := 0; j < size; j++ {
    			data[j] = rand.Int()
    		}
            // 调用函数时启动计时
    		b.StartTimer()
    		sortAndTotal(data)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    断言(assertion)

    go测试没有提供断言,对于java开发人员来说有点不习惯。这里介绍第三方库 github.com/stretchr/testify/assert.它提供了一组易理解的测试工具。

    assert示例

    assert子库提供了便捷的断言函数,可以大大简化测试代码的编写。总的来说,它将之前需要判断 + 信息输出的模式:

    import (
      "testing"
      "github.com/stretchr/testify/assert"
    )
    
    func TestSomething(t *testing.T) {
    
      var a string = "Hello"
      var b string = "Hello"
    
      assert.Equal(t, a, b, "The two words should be the same.")
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    观察到上面的断言都是以TestingT为第一个参数,需要大量使用时比较麻烦。testify提供了一种方便的方式。先以testing.T创建一个Assertions对象,Assertions定义了前面所有的断言方法,只是不需要再传入TestingT参数了。

    func TestEqual(t *testing.T) {
      assertions := assert.New(t)
      assertion.Equal(a, b, "")
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    TestingT类型定义如下,就是对*testing.T做了一个简单的包装:

    // TestingT is an interface wrapper around *testing.T
    type TestingT interface {
    	Errorf(format string, args ...interface{})
    }
    
    • 1
    • 2
    • 3
    • 4

    下面引用官网的一个示例。

    首先定义功能函数Addition:

    func Addition(a, b int) int {
    	return a + b
    }
    
    • 1
    • 2
    • 3

    测试代码:

    import (
    	"github.com/stretchr/testify/assert"
    	"testing"
    )
    
    // 定义比较函数类型,方便后面批量准备测试数据
    type ComparisonAssertionFunc func(assert.TestingT, interface{}, interface{}, ...interface{}) bool
    
    // 测试参数类型
    type args struct {
    	x int
    	y int
    }
    
    func TestAddition(t *testing.T) {
    
    	tests := []struct {
    		name      string
    		args      args
    		expect    int
    		assertion ComparisonAssertionFunc
    	}{
    		{"2+2=4", args{2, 2}, 4, assert.Equal},
    		{"2+2!=5", args{2, 2}, 5, assert.NotEqual},
    		{"2+3==5", args{2, 3}, 5, assert.Exactly},
    	}
    
    	for _, tt := range tests {
            // 动态执行断言函数
    		t.Run(tt.name, func(t *testing.T) {
    			tt.assertion(t, tt.expect, Addition(tt.args.x, tt.args.y))
    		})
    	}
    	assert.Equal(t, 2, Addition(1, 1), "sum result is equal")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
  • 相关阅读:
    基于JAVA网络作业提交与批改系统计算机毕业设计源码+数据库+lw文档+系统+部署
    数据栅格化
    Iphone文件传到电脑用什么软件,看这里
    iPhone 12电池寿命结果:四款机型都进行了比较
    原生js写菜单栏滑块动画+Banner滑动效果(清晰思路+附代码)
    每日4道算法题——第029天
    【Spark NLP】第 10 章:主题建模
    时隔10年谷歌计划重启谷歌实验室,聚焦AR、VR项目
    element组件选择器的下拉框样式问题
    Redis篇---第十三篇
  • 原文地址:https://blog.csdn.net/neweastsun/article/details/128101741