当前位置 博文首页 > 小码哥说测试的博客:一篇文章让你学会写golang 单元测试、基准

    小码哥说测试的博客:一篇文章让你学会写golang 单元测试、基准

    作者:[db:作者] 时间:2021-08-24 18:55

    目录

    golang 单元测试、基准测试、子测试、并发测试基础教程

    一、go test基础

    二、准备

    三、单元测试

    四、基准测试

    五、并发测试

    1、可并发执行的测试用例

    2、基于子测试的并发单元测试

    3、并发基准测试

    六、示例功能

    七、总结


    golang 单元测试、基准测试、子测试、并发测试基础教程

    一、go test基础

    用法: go test [build/test flags] [packages] [build/test flags & test binary flags]

    Go test 会自动测试由导入路径命名的软件包。并以下格式打印测试结果的摘要:

    ok archive/tar 0.011s

    FAIL archive/zip 0.022s

    ok compress/gzip 0.033s

    ...

    Go test 会重新编译每个软件包以及名称匹配的所有文件名形如 * _test.go 的测试文件。这些文件可以包含测试函数,基准测试函数和示例函数(test functions, benchmark functions, example functions)。更多有关信息,请参见“ go help testfunc”。

    每个列出的包都执行单独的测试二进制文件。其中名称以“_”(包括“_test.go”)或“.”开头的文件将被忽略。而后缀为“_test”的测试文件将被编译为单独的程序包,并与主测试文件链接并执行。

    go工具将忽略名为“testdata”的目录,该目录可用来存放测试所需的辅助数据。

    作为构建测试二进制文件的一部分,go test 会对软件包及其测试文件执行run vet,用于检测其中是否存在重大问题。如果执行go vet阶段发现任何问题,会直接报告这些问题,并且不运行测试文件。检查内容只包括go vet检查的一个子集,包括:'atomic', 'bool', 'buildtags', 'errorsas',
    'ifaceassert', 'nilfunc', 'printf', and 'stringintconv'。可以通过“go doc cmd/vet”查看 vet检查的相关文档。如果要禁用go vet的运行,请使用-vet=off标志。

    所有测试输出和摘要行都打印到go命令的标准输出,即使测试将其打印为自己的标准错误。(go命令的标准错误保留用于打印建立测试时出错)。

    Go test 以两种不同的模式运行:

    第一种称为本地目录模式,执行go test时无需指定package参数调用(例如,“go test”或“go test -v')。在这种模式下,go test会编译当前包,然后在当前目录中找到测试用例。缓存(如下所述)会被被禁用。包测试完成后,去打印摘要行显示测试状态(“ok”或“FAIL”),软件包名称和使用时间。

    第二种称为包列表模式,在调用go test时需要明确package参数(例如“go test math”,“go test./...”,或者“go test .”)。在这种模式下,将编译并测试命令行上列出的每个软件包。如果一个包测试通过,仅打印最终的“ok”摘要。如果包测试失败,则打印完整的测试输出。如果使用-bench或-v标志调用,则go test会打印完整的输出(包括通过测试的包),以显示要求的基准测试结果或详细的日志记录。在所有待测试包全部测试完毕,同时测试结果打印完成后,如果有任何一个测试未通过,则最后会打印一个’FAIL‘。

    在第二种运行模式下,go test会缓存成功的包测试结果,以避免不必要的重复测试。当测试结果可以从缓存恢复时,执行go test将显示先前的测试结果。当发生这种情况时,结果将输出(cached)代替测试执行时间这一参数。(文末有作者这些年总结的学习笔记)

    二、准备

    接下来,将通过一个个例子说明单元测试、基准测试、子测试、并发测试到底该如何写,在此之前,我们需要先准备一个待测试的小功能。这里,我们以一个对全局map变量执行增删改查的功能为例,开发对应的测试代码。具体如下:

    var Cash = make(map[string]string)
    
    func Add(key,value string){
        if _,ok := Cash[key];!ok{
            Cash[key] = value
        }
    }
    
    func Delete(key string){
        if _,ok := Cash[key];ok{
            delete(Cash,key)
        }
    }
    
    func Update(key,value string){
        Cash[key] = value
    }
    
    func Get(key string) string{
        if v,ok := Cash[key];ok{
            return v
        }
        return ""
    }
    
    func Clean(){
        Cash = make(map[string]string)
    }
    
    

    这里我们可以没有为Cash变量添加锁机制,目的是为了在验证阶段通过并发测试找出这个’bug‘。另外,还额外提供了一个Clean()方法,用于清空变量的内容。

    三、单元测试

    单元测试是最简单,也是最基本的测试。例如,我们想要测试add和get功能是否正常,通常我们会这样写。

    func TestAddAndGet(t *testing.T){
        Add("a","aa")
        fmt.Println(Get("a"))
    }
    

    这是最基本的测试方法,但是效率太低,同时需要开发者自己去判断结果是否正确。通常会通过Table-driven的方式实现这种简单重复的测试内容,我们可以按批次执行测试,并通过程序化的方式验证测试结果,具体如下:

    func TestAdd(t *testing.T){
        var addTests = []struct{
            key string
            value string
            expected int
        }{
            {"a","aa",1},
            {"b","bb",2},
            {"c","cc",3},
            {"c","cc",3},
            {"c","cc",3},
            {"d","dd",4},
            {"e","ee",5},
            {"f","ff",6},
            {"g","gg",7},
            {"h","hh",8},
            {"i","ii",9},
            {"j","jj",10},
        }
    
        quary := rand.Int()
        for _,v := range addTests{
            Add(v.key,v.value)
            t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
            if len(Cash) != v.expected{
                t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
            }
        }
        Clean()
    }
    

    如果某次插入操作出现异常会返回错误信息:

    === RUN   TestAdd
        basic_test.go:48: add d:dd len = 4; except 4
    --- FAIL: TestAdd (0.00s)
    FAIL   
    

    四、基准测试

    基准测试,通常也被成为压测Benchmark。压测通常会自动的顺序执行多次,然后返回平均的压测参数。例如我们测试get方法的性能:

    func BenchmarkGet(b *testing.B) {
    
        b.Log("start")
        var addTests = []struct{
            key string
            value string
            expected int
        }{
            {"a","aa",1},
            {"b","bb",2},
            {"c","cc",3},
            {"c","cc",3},
            {"c","cc",3},
            {"d","dd",4},
            {"e","ee",5},
            {"f","ff",6},
            {"g","gg",7},
            {"h","hh",8},
            {"i","ii",9},
            {"j","jj",10},
        }
        for _,v := range addTests{
            Add(v.key,v.value)
        }
    
        //启动内存统计
        b.ReportAllocs()
    
        //重新计时
        b.ResetTimer()
    
        for i:=0;i<b.N;i++{
            var result []string
            for _,v := range addTests{
                value := Get(v.key)
                if value != v.value{
                    b.Errorf("get %s:%s, except %s",v.key, value,v.value)
                }
                result = append(result,value)
            }
        }
    
    }
    

    测试结果为:

    goos: darwin
    goarch: amd64
    pkg: test-learn
    BenchmarkGet
        basic_test.go:185: start
        basic_test.go:185: start
        basic_test.go:185: start
        basic_test.go:185: start
        basic_test.go:185: start
    BenchmarkGet-12      2162974           546 ns/op         496 B/op          5 allocs/op
    PASS
    
    

    通过分析测试结果可知,测试流程共计执行5次,测试结果为平均值。benchmark允许自定义测试计时的起止时间,默认测试代码启动开始计时,测试结束停止计时。在本例中,由于我们想要测试get方法性能,因此排除前期数据插入时间。性能参数会更加准确,因此我们通过b.ResetTimer()初始化了计时器开始的时间。

    如果我们关注的流程在测试代码前半段时呢?testing包提供了更加灵活的手动计时功能。

    b.StartTimer()
    b.StopTimer()
    

    此外,testing支持内存统计功能,b.ReportAllocs()相当于在 go test 时添加-benchmem 标识。

    2162974 :基准测试的迭代总次数 b.N

    546 ns/op:平均每次迭代所消耗的纳秒数

    496 B/op:平均每次迭代内存所分配的字节数

    5 allocs/op:平均每次迭代的内存分配次数

    五、并发测试

    执行并发测试有两种方式,一种是单元并发测试,另一种是基准并发测试。接下来我们将对两种并发测试方式分别进行介绍。再次之前我们需要先介绍一下 t.Parallel() 参数,该参数指明当前测试用例可与其他可并行执行的测试用例一起运行,仅仅声明了测试用例的属性,并不会真的去完成并发测试。

    1、可并发执行的测试用例

    为了更清楚的说明可并发执行t.Parallel() 属性的意义,我们通过以下例子说明。假如我们有两个测试插入数值的测试用例:

    func TestCanParallelExecAdd(t *testing.T){
        var addTests = []struct{
            key string
            value string
            expected int
        }{
            {"a","aa",1},
            {"b","bb",2},
            {"c","cc",3},
            {"c","cc",3},
            {"c","cc",3},
            {"d","dd",4},
            {"e","ee",5},
            {"f","ff",6},
            {"g","gg",7},
            {"h","hh",8},
            {"i","ii",9},
            {"j","jj",10},
        }
    
        t.Parallel()
    
        quary := rand.Int()
        t.Logf("[goroutine:%d] start",quary)
    
        for _,v := range addTests{
            Add(v.key,v.value)
            t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
            if len(Cash) != v.expected{
                t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
            }
        }
        Clean()
    }
    
    
    func TestCanParallelExecAdd2(t *testing.T){
        var addTests = []struct{
            key string
            value string
            expected int
        }{
            {"a","aa",1},
            {"b","bb",2},
            {"c","cc",3},
            {"c","cc",3},
            {"c","cc",3},
            {"d","dd",4},
            {"e","ee",5},
            {"f","ff",6},
            {"g","gg",7},
            {"h","hh",8},
            {"i","ii",9},
            {"j","jj",10},
        }
    
        t.Parallel()
    
        quary := rand.Int()
        t.Logf("[goroutine:%d] start",quary)
    
        for _,v := range addTests{
            Add(v.key,v.value)
            t.Logf("[goroutine:%d] add %s:%s",quary,v.key,v.value)
            if len(Cash) != v.expected{
                t.Errorf("add %s:%s len = %d; except %d",v.key,v.value,len(Cash),v.expected)
            }
        }
        Clean()
    }
    
    

    这两个测试用例除了名字,内部实现完全一样,然后我们执行 go test 会返回错误: fatal error: concurrent map read and map write。 如果我们将t.Parallel()注释,或者将其中一个测试用例注释掉,将会通测试。

    这是因为两个测试用例都声明了可并发执行,当我们执行go test时,两个测试用例将并发执行,而map不是线程安全的数据结构,因此会爆出异常。

    到这里,基准并发测试的实现方式已经十分清晰,但是这需要十分冗余的测试代码,如果想要测试10并发量的测试难道要写10份逻辑一样的测试代码吗?

    当然不是,testing包提供了子测试的概念,可以便于我们实现更加复杂的测试逻辑。

    2、基于子测试的并发单元测试

    子测试是指我们可以在单元测试中启动多个测试用例,具体如下:

    func TestParallelAdd1(t *testing.T){
        for i:=0;i<10;i++{
            t.Run(fmt.Sprintf("g-%d",i), TestAdd)
        }
    }
    

    我们通过t.run函数,批量启动了10个子测试用例。通过日志我们发现,这10个子测试用例是按顺序执行的,这是因为TestAdd单元测试并不支持并发执行,接下来我们对上述代码做出修改,为子测试用例添加可并发属性。

    func TestParallelAdd(t *testing.T){
        for i:=0;i<10;i++{
            t.Run(fmt.Sprintf("g-%d",i), func(t *testing.T) {
                t.Parallel()
                TestAdd(t)
            })
        }
    }
    

    再次执行,发生了fatal error: concurrent map writes的异常,测试未通过。

    3、并发基准测试

    与单元测试不同,基准测试提供了并发测试的方法b.RunParallel,具体如下:

    func BenchmarkParallelAdd(b *testing.B) {
    
        b.Log("start")
        var process uint32 = 0
        var count uint64 = 0
    
        b.SetParallelism(2)
    
        b.RunParallel(func(pb *testing.PB) {
    
            temp := atomic.AddUint32(&process,1)
    
            b.Logf("[goroutine:%d] start",temp)
            for pb.Next() {
                atomic.AddUint64(&count,1)
                // The loop body is executed b.N times total across all goroutines.
                b.Logf("[goroutine:%d] count=%d",temp,atomic.LoadUint64(&count))
    
            }
            b.Logf("[goroutine:%d] end",temp)
        })
    
    }
    
    
    

    通过 RunParallel 方法能够并行地执行给定的基准测试。RunParallel会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用SetParallelism(如 SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。

    六、示例功能

    testing还提供了示例功能,一般用于展示某些功能的示例,此外也常用于测试。示例通常以example_test.go命名文件,示例函数通常以ExampleXxx_xxx命名,下划线及其后内容不是必须的。具体如下:

    func ExampleGet() {
        Add("a","aa")
        Add("b","bb")
        fmt.Println(Get("a"))
        fmt.Println(Get("b"))
    
        // Output:
        // aa
        // bb
    }
    

    七、总结

    通常测试用例文件被建议与源文件写在同一个包中。

    测试常用命令

    go test 执行当前包中全部测试用例,不包括 benchmark测试

    go test -bench=. 执行当前包中全部测试用例,包括 benchmark测试

    go test -v 执行测试用例时打印测试详情

    go test -race 检查当前测试代码是否存在竞争异常,用于检查线程安全

    go test -cover 检查测试代码覆盖率

    最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:包括,软件学习路线图,50多天的上课视频、16个突击实战项目,80余个软件测试用软件,37份测试文档,70个软件测试相关问题,40篇测试经验级文章,上千份测试真题分享,还有2021软件测试面试宝典,还有软件测试求职的各类精选简历,希望对大家有所帮助…

    想要获取上方这套学习资料(都是免费获取的~)
    添加我们的小姐姐即可
    可不能撩我们的小姐姐哦
    

    码字不易,文章对你有帮助的话,点个赞收个藏,给作者一个鼓励。也方便你下次能够快速查找。

    cs