• 18-Go语言之单元测试


    go test工具

    Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法或工具。

    go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

    *_test.go文件中有三种类型的函数,单元测试函数,基准测试函数和示例函数。

    类型格式作用
    测试函数函数名前缀为Test测试程序的一些逻辑行为是否正确
    基准函数函数名前缀为Benchmark测试函数的性能
    示例函数函数名前缀为Example为文档提供示例文档

    go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理测试中生成的临时文件。

    测试函数

    测试函数的格式

    每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

    func TestName(t *testing.T){
        // ...
    }
    
    • 1
    • 2
    • 3

    测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:

    func TestAdd(t *testing.T){ ... }
    func TestSum(t *testing.T){ ... }
    func TestLog(t *testing.T){ ... }
    
    • 1
    • 2
    • 3

    其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    测试函数示例

    接下来我们定义一个split的包,包中定义了一个Split的函数,具体实现如下:

    package main
    
    import "strings"
    
    func Split(s, seq string) (res []string) {
    	i := strings.Index(s, seq)
    
    	for i > -1 {
    		res = append(res, s[:i])
    		s = s[i+1:]
    		i = strings.Index(s, seq)
    	}
    	res = append(res, s)
    	return
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:

    import (
    	"reflect"
    	"testing"
    )
    
    func TestSplit(t *testing.T) { //测试函数名必须以Test开头,必须接受一个*testing.T类型的参数
    	got := Split("a:b:c", ":")      //程序输出的结果
    	want := []string{"a", "b", "c"} //期望的结果
    
    	if !reflect.DeepEqual(want, got) { //因为slice不能直接比较,借助反射包的方法比较
    		t.Errorf("excepted: %v, got: %v", want, got)
    
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在当前目录下,执行go test命令,可以看到结果:

    sh-3.2$ go test
    PASS
    ok      day08/lock/test 0.659s
    
    • 1
    • 2
    • 3

    一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数:

    func TestMoreSplit(t *testing.T) {
    	got := Split("abcd", "bc")
    	want := []string{"a", "d"}
    	if reflect.DeepEqual(want, got) {
    		t.Errorf("expected:%v, got:%v", want, got)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再次执行go test命令,输出结果如下:

    sh-3.2$ go test
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:23: expected:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    day08/lock/test 0.330s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们可以为go test命令添加-v参数,查看测试函数名称和运行时间:

    sh-3.2$ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
        split_test.go:23: expected:[a d], got:[a cd]
    --- FAIL: TestMoreSplit (0.00s)
    FAIL
    exit status 1
    FAIL    day08/lock/test 0.269s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    还可以在go test命令添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。

    sh-3.2$ go test -v -run="More"
    === RUN   TestMoreSplit
        split_test.go:23: expected:[a d], got:[a cd]
    --- FAIL: TestMoreSplit (0.00s)
    FAIL
    exit status 1
    FAIL    day08/lock/test 0.571s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    现在我们回过头来解决我们程序中的问题。很显然我们最初的split函数并没有考虑到sep为多个字符的情况,我们来修复下这个Bug:

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这一次我们再来测试一下,我们的程序。注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。

    sh-3.2$ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    ok      day08/lock/test 0.552s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这一次我们的测试都通过了。

    测试组

    我们还想测试下split函数对中文字符串的支持,我们可以在写一个函数,但是我们也可以使用如下更友好的一种方式来添加更多的应用实例。

    func TestSplit(t *testing.T) {
       // 定义一个测试用例类型
    	type test struct {
    		input string
    		sep   string
    		want  []string
    	}
    	// 定义一个存储测试用例的切片
    	tests := []test{
    		{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
    		{input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
    		{input: "abcd", sep: "bc", want: []string{"a", "d"}},
    		{input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
    	}
    	// 遍历切片,逐一执行测试用例
    	for _, tc := range tests {
    		got := Split(tc.input, tc.sep)
    		if !reflect.DeepEqual(got, tc.want) {
    			t.Errorf("expected:%v, got:%v", tc.want, got)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    我们通过上面的代码把多个测试用例合到一起,再次执行go test命令。

    split $ go test -v
    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:42: expected:[河有 又有河], got:[ 河有 又有河]
    FAIL
    exit status 1
    FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    测试覆盖率

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

    Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

    split $ go test -cover
    PASS
    coverage: 100.0% of statements
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s
    
    • 1
    • 2
    • 3
    • 4

    从上面的结果可以看到我们的测试用例覆盖了100%的代码。

    Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

    split $ go test -cover -coverprofile=c.out
    PASS
    coverage: 100.0% of statements
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s
    
    • 1
    • 2
    • 3
    • 4

    上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中,然后我们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。

    基准测试

    我们为split包中的Split函数编写基准测试如下:

    func BenchmarkSplit(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		Split("沙河有沙又有河", "沙")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:

    split $ go test -bench=Split
    goos: darwin
    goarch: amd64
    pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               203 ns/op
    PASS
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       2.255s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000203ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。

    我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

    split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               215 ns/op             112 B/op          3 allocs/op
    PASS
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       2.394s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。 我们将我们的Split函数优化如下:

    func Split(s, sep string) (result []string) {
    	result = make([]string, 0, strings.Count(s, sep)+1)
    	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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:

    split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               127 ns/op              48 B/op          1 allocs/op
    PASS
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       1.423s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配。

    格式要求

    1. 测试文件的名字 : xx_test.go
    2. 测试函数的名字:TestXxx(t *testing.T)
  • 相关阅读:
    ITSM | 对话龙智资深技术顾问,探讨ITSM实践如何从过去转向未来
    3分钟带你了解前端缓存-HTTP缓存
    C#winform软件实现一次编译,跨平台windows和linux兼容运行,兼容Visual Studio原生界面Form表单开发
    /etc/sudoers文件未配置nopasswd:但sudo su没有输密码就直接进root了
    开源文本嵌入模型M3E
    Docker与Kubernetes结合的难题与技术解决方案
    这可能是最全的Web测试各个测试点,有这一篇就够了
    SkyWalking 入门教程
    Android泛型详解
    Springboot网络微小说的设计与实现毕业设计源码031758
  • 原文地址:https://blog.csdn.net/weixin_38753143/article/details/125620252