• GO语言测试


    Go语言的测试技术是相对低级的。它依赖一个 go test 测试命令和一组按照约定方式编写的 测试函数,测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的,而且它很容易延伸到基准测试和示例文档。

    go test

    编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
    在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

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

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

    测试函数

    格式:测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,参数t用于报告测试失败和附加的日志信息。

    func TestName(t *testing.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
    

    示例:自定义一个split函数,实现split功能:

    func Split(s,sep string) (result []string)  {
        i := strings.Index(s,sep)
        for i > -1{
            result = append(result,s[:i])
            s = s[i+1:]
            i = strings.Index(s,sep)
        }
        result =append(result,s)
        return 
    }
    

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

    func TsetSplit(t *testing.T)  {
        got := Split("a:b:c",":")
        want := []string{"a","b","c"}
        if !reflect.DeepEqual(want,got){
            t.Errorf("期待:%v,得到:%v",want,got)
        }
    }
    

    在split包路径下,执行go test命令,可以看到输出结果如下:

    testing: warning: no tests to run
    PASS
    

    出现pass意味测试通过!
    如果有多个测试函数,其中一个测试不过,会出现以下提示:

    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:40: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    

    如果要查看测试函数名称和运行时间,可以使用go test -v

    === RUN   TestMoreSplit
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:40: excepted:[a d], got:[a cd]
    FAIL
    

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

    测试组

    如果有多个测试,不必每个测试都写一个函数,可以使用测试组来完成。

    //测试组
    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:"abcdbce",sep:"bc",want:[]string{"a","d","e"}},
            {input:"上海自来水来自海上",sep:"海",want:[]string{"上","自来水来自","上"}},
        }
        for _,tc := range tests{
            got := Split(tc.input,tc.sep)
            if !reflect.DeepEqual(got,tc.want){
                t.Errorf("期待:%v,获得:%v",tc.want,got)
            }
        }
    }
    

    执行go test -v,获得

    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:59: 期待:[a b c],获得:[a:b:c]
        split_test.go:59: 期待:[a d e],获得:[a cd ce]
        split_test.go:59: 期待:[上 自来水来自 上],获得:[上 ��自来水来自 ��上]
    FAIL
    exit status 1
    

    中文有乱码,这种情况下可以使用%#v的格式化方式查看乱码是什么:
    t.Errorf("期待:%#v,获得:%#v",tc.want,got)
    得到:
    split_test.go:59: 期待:[]string{"上", "自来水来自", "上"},获得:[]string{"上", "xb5xb7自来水来自", "xb5xb7上"}

    当测试组中测试比较多的时候,可以加个name来查看是哪个测试用例出了问题:

    //测试组
    func TestSplit(t *testing.T) {
        type test struct{  // 定义test结构体
            input string
            sep string
            want []string
        }
        tests := map[string]test{
            "simple":   {input:"a:b:c",sep:":",want:[]string{"a","b","c"}},
            "simple1":  {input:"a:b:c",sep:",",want:[]string{"a","b","c"}},
            "simple2":  {input:"abcdbce",sep:"bc",want:[]string{"a","d","e"}},
            "simple3":  {input:"上海自来水来自海上",sep:"海",want:[]string{"上","自来水来自","上"}},
    
        }
        for name,tc := range tests{
            got := Split(tc.input,tc.sep)
            if !reflect.DeepEqual(got,tc.want){
                t.Errorf("测试名:%#v,期待:%#v,获得:%#v",name,tc.want,got)
            }
        }
    }
    

    输出的时候就会把测试用例的名字带上:

    === RUN   TestSplit
    --- FAIL: TestSplit (0.00s)
        split_test.go:66: 测试名:"simple1",期待:[]string{"a", "b", "c"},获得:[]string{"a:b:c"}
        split_test.go:66: 测试名:"simple2",期待:[]string{"a", "d", "e"},获得:[]string{"a", "cd", "ce"}
        split_test.go:66: 测试名:"simple3",期待:[]string{"上", "自来水来自", "上"},获得:[]string{"上", "xb5xb7自来水来自", "xb5xb7上"}
    FAIL
    exit status 1
    

    子测试

        for name,tc := range tests{
            t.Run(name, func(t *testing.T) {
                got := Split(tc.input,tc.sep)
                if !reflect.DeepEqual(got,tc.want){
                    t.Errorf("测试名:%#v,期待:%#v,获得:%#v",name,tc.want,got)
                }
            })
        }
    

    执行结果:

    $go test -v
    === RUN   TestSplit
    === RUN   TestSplit/simple2
    === RUN   TestSplit/simple3
    === RUN   TestSplit/simple
    === RUN   TestSplit/simple1
    --- FAIL: TestSplit (0.00s)
        --- FAIL: TestSplit/simple2 (0.00s)
            split_test.go:71: 测试名:"simple2",期待:[]string{"a", "d", "e"},获得:[]string{"a", "cd", "ce"}
        --- FAIL: TestSplit/simple3 (0.00s)
            split_test.go:71: 测试名:"simple3",期待:[]string{"上", "自来水来自", "上"},获得:[]string{"上", "xb5xb7自来水来自", "xb5xb7上"}
        --- PASS: TestSplit/simple (0.00s)
        --- FAIL: TestSplit/simple1 (0.00s)
            split_test.go:71: 测试名:"simple1",期待:[]string{"a", "b", "c"},获得:[]string{"a:b:c"}
    FAIL
    exit status 1
    

    可以通过-run=RegExp来指定运行的测试用例,还可以通过/来指定要运行的子测试用例,只测试simple:
    $go test -v -run=Split/simple

    测试覆盖率

    在测试中至少被运行一次的代码占总代码的比例。Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。

    $go test -cover
    --- FAIL: TestSplit (0.00s)
        --- FAIL: TestSplit/simple2 (0.00s)
            split_test.go:71: 测试名:"simple2",期待:[]string{"a", "d", "e"},获得:[]string{"a", "cd", "ce"}
        --- FAIL: TestSplit/simple3 (0.00s)
            split_test.go:71: 测试名:"simple3",期待:[]string{"上", "自来水来自", "上"},获得:[]string{"上", "xb5xb7自来水来自", "xb5xb7上"}
        --- FAIL: TestSplit/simple1 (0.00s)
            split_test.go:71: 测试名:"simple1",期待:[]string{"a", "b", "c"},获得:[]string{"a:b:c"}
    FAIL
    coverage: 100.0% of statements
    exit status 1
    

    Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。
    $ go test -cover -coverprofile=c.out
    执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。

    用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

    基准测试

    基准测试就是在一定的工作负载之下检测程序性能的一种方法。
    格式:

    func BenchmarkName(b *testing.B){
        // ...
    }
    

    基准测试以Benchmark为前缀,需要一个'*testing.B'类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:

    func (c *B) Error(args ...interface{})
    func (c *B) Errorf(format string, args ...interface{})
    func (c *B) Fail()
    func (c *B) FailNow()
    func (c *B) Failed() bool
    func (c *B) Fatal(args ...interface{})
    func (c *B) Fatalf(format string, args ...interface{})
    func (c *B) Log(args ...interface{})
    func (c *B) Logf(format string, args ...interface{})
    func (c *B) Name() string
    func (b *B) ReportAllocs()
    func (b *B) ResetTimer()
    func (b *B) Run(name string, f func(b *B)) bool
    func (b *B) RunParallel(body func(*PB))
    func (b *B) SetBytes(n int64)
    func (b *B) SetParallelism(p int)
    func (c *B) Skip(args ...interface{})
    func (c *B) SkipNow()
    func (c *B) Skipf(format string, args ...interface{})
    func (c *B) Skipped() bool
    func (b *B) StartTimer()
    func (b *B) StopTimer()
    

    为split编写基准测试:

    //基准测试
    func BenchmarkSplit(b *testing.B) {
        for i:=0;i<b.N;i++{
            Split("a:b:c",":")
        }
    }
    

    通过执行go test -bench=Split命令执行基准测试,输出结果如下:

    $go test -bench=Split
    goos: darwin
    goarch: amd64
    pkg: xxxxxx
    BenchmarkSplit-8        10000000               203 ns/op
    PASS
    

    其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000和203ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。
    默认情况下,每个基准测试至少运行1秒!
    可为基准测试添加-benchmem参数,来获得内存分配的统计数据。

    $go test -bench=Split -benchmem
    ...
    BenchmarkSplit-8        10000000               203 ns/op             112 B/op          3 allocs/op
    ...
    

    112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。

    性能比较函数

    性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。

    func benchmark(b *testing.B, size int){/* ... */}
    func Benchmark10(b *testing.B){ benchmark(b, 10) }
    func Benchmark100(b *testing.B){ benchmark(b, 100) }
    func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
    

    使用斐波那契数的函数测试:

    // Fib 是一个计算第n个斐波那契数的函数
    func Fib(n int) int  {
        if n <2{
            return n
        }
        return Fib(n-1) + Fib(n+2)
    }
    

    性能比较函数:

    func benchmarkFib(b *testing.B, n int) {
        for i := 0; i < b.N; i++ {
            Fib(n)
        }
    }
    func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
    func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
    func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
    func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
    func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
    func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
    

    默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。
    还可以使用-benchtime标志增加最小基准时间,以产生更准确的结果。

    并行测试

    func (b *B) RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。 RunParallel会创建出多个goroutine,并将b.N分配给这些 goroutine 执行, 其中 goroutine数量的默认值为GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelism 。RunParallel通常会与-cpu标志一同使用。
    go test -bench=. -cpu 1来指定使用的CPU数量。

  • 相关阅读:
    【Azure 事件中心】使用Kafka消费Azure EventHub中数据,遇见消费慢的情况可以如何来调节呢?
    【Azure 媒体服务】记录使用Java调用Media Service API时候遇见的一些问题
    【Azure 应用服务】App Service 的.NET Version选择为.NET6,是否可以同时支持运行ASP.NET V4.8的应用呢?
    【Azure 应用服务】应用服务中发布Docker Container,如何添加卷(如Azure File Share)以便永久存储文件
    【Azure 应用服务】在App Service中调用外部服务API时需要携带客户端证书,而多次调用的情况下会出现WindowsCryptographicException Keyset does not exist异常
    【Azure 事件中心】在Azure Function App中消费Event Hub数据,时常出现EventReceiveError
    【Azure 环境】IntelliJ IDEA Community Edition 2021.2.3登陆Azure账号时,无法切换到中国区
    【Azure Redis 缓存】Azure Redis Cluster 在增加分片数时失败分析
    【Azure 应用服务】Storage Queue触发Azure Function时报错 The input is not a valid Base64 string
    【Azure API 管理】使用APIM进行XML内容读取时遇见的诡异错误 Expression evaluation failed. Object reference not set to an instance of an object.
  • 原文地址:https://www.cnblogs.com/show58/p/12606954.html
Copyright © 2020-2023  润新知