Go语言单元测试超详细解析

 更新时间:2022年02月25日 16:29:06   作者:酷尔。  
本文介绍了了Go语言单元测试超详细解析,测试函数分为函数的基本测试、函数的组测试、函数的子测试,进行基准测试时往往是对函数的算法进行测验,有时后一个算法在测试数据的基量不同时测试出的效果会不同我们需要对不同数量级的样本进行测试,下文需要的朋友可以参考下

前言:

平时根据需求写代码.人工进行测试往往不会面面俱到,还会因为需求的改变繁琐的进行测试
通过完成一个测试函数,可以大大简化测试的步骤,并且在需求该变的时候只需要改变一下测试的输入与期望

一、单元测试分类及其概念

1.基本分类

  • 测试函数 函数前缀为Test 主要用于测试程序的一些逻辑行为是否正确
  • 基准函数 函数名前缀为Benchmark 主要测试函数的性能
  • 示例函数 函数的前缀名为 Example 为文档提示示例文档

2.细说单元测试分类

①测试函数

  • 函数的基本测试
  • 函数的组测试
  • 函数的子测试
  • 测试函数覆盖率(也就是被测试函数有多少代码用于了执行)
  • 在进行测试的时候,要保证被测函数覆盖率为100%,测试函数覆盖率为60%以上,否则写的代码大部分用不到,需要优化
    • ①测试覆盖率可以使用go test -cover
    • go test -cover -coverprofile=c.out(将测试的结果存入文件c.out)
    • 然后使用go tool cover -html=c.out可以打开文件,显示哪些代码没有执行

②基准测试

  • 基准函数测试是做什么的:基准函数会自定义一个时间段用于执行代码,如果代码简洁,被测函数的执行次数需要成倍增加(直到达到基准测试函数给的预期,然后统计一共执行了多少轮,,每轮平均用时多少)
  • 执行基准函数时,首先要写基准函数
  • 基准测试函数的参数是:*testing.B对应的指针变量
  • 在测试函数内部要进行循环,循环的终止条件是b.N

二、结合代码细说每一种测试

1.基准测试

(1)基准测试函数需要注意的点

进行基准测试时往往是对函数的算法进行测验,有时后一个算法在测试数据的基量不同时测试出的效果会不同我们需要对不同数量级的样本进行测试的时候。可以自己写一个盒子作为跳板,测试什么数量级的数据就只进行当前数量级的测试

 测试的时候命令为:go test -bench=.(将所有的跳板函数均执行一遍)或者=特定函数进行特定函数测试-benchtime=time  (在被测试函数在基准函数默认的时间内执行不完时可以拿这个参数进行时间的扩充)在进行基准函数测试的时候,可能需要进行一些前置工作,如果感觉前置工作浪费时间可以使用 b.ResetTimer()对计时器进行重置

(2)基准测试代码

测试函数代码如下:

//文件名 subStr_test.go
package main

import (
    "reflect"
    "testing"
)
func BenchmarkSubStr(b *testing.B) {
    for i := 0; i < b.N; i++ {
        res := subStr("qwe:qw:es:wqe", ":")
        if !reflect.DeepEqual(res, []string{"qwe", "qw", "es", "wqe"}) {
            b.Errorf("不匹配")
        }
    }

}

func benchmarkFib(b *testing.B, n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}

func BenchmarkFib5(b *testing.B)  { benchmarkFib(b, 5) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib15(b *testing.B) { benchmarkFib(b, 15) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }

被测函数代码如下:

//文件名 subStr.go
package main

import (
    "fmt"
    "strings"
)

func subStr(str, stre string) []string {
    index := strings.Index(str, stre)
    var theSub []string

    for index >= 0 {
        // 先将分隔符前面的数据进行保存
        temp := str[:index]
        // 将字符串往后移
        str = str[index+1:]
        // 重新获取下标
        index = strings.Index(str, stre)
        if temp != "" {
            theSub = append(theSub, temp)
        } else {
            continue
        }
    }
    theSub = append(theSub, str)
    return theSub[:]
}

// 菲薄那契数列
func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

func main() {
    fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Printf("%#v\n", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Println(123)
}

将以上两个文件放在同一目录下,执行测试命令 go test
 

得到测试结果:

    goos: windows
    goarch: amd64
    cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
    BenchmarkSubStr-8   1213681       1012 ns/op      352 B/op  14 allocs/op
    PASS
    ok      _/d_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/基准测试    2.410s

2.组测试与子测试

  • 对于一个函数的测试,不仅仅是一组测试用例,所以我们需要写测试组, 对一个函数进行测试,测试的时候一般用集合键值对构成,每一个键对应一组测试数据
  • 进行组测试的时候可能会有个别的组出错,所以使用子测试可以对某个案例进行单独测试这就是测试组与子测试的产生背景

测试函数如下:

package main

import (
    "reflect"
    "testing"
)

type testS struct {
    str  string
    ste  string
    want []string
}

//进行组测试,批量进行测试,如果有哪个地方不满足则进行异常抛出
// func TestSubStr(t *testing.T) {
//     testMap := map[string]testS{
//         "case_1": {"123:eqwe:123", ":", []string{"123", "eqwe", "123"}},
//         "case_2": {"13:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
//         "case_3": {"12@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
//         "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
//         "case_5": {"123:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
//         "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
//     }
//     for k, v := range testMap {
//         res := subStr(v.str, v.ste)
//         if !reflect.DeepEqual(res, v.want) {
//             t.Errorf("%v want:%#v got:%#v", k, v.want, res)
//         }
//     }
// }

// 进行子测试,可以针对某个子样本进行测试
func TestSubStr(t *testing.T) {
    testMap := map[string]testS{
        "case_1": {"13:eqwe:123", ":", []string{"123", "eqwe", "123"}},
        "case_2": {"3:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
        "case_3": {"2@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
        "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
        "case_5": {"23:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
        "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
    }
    for k, v := range testMap {
        t.Run(k, func(t *testing.T) {
            res := subStr(v.str, v.ste)
            if !reflect.DeepEqual(res, v.want) {
                t.Errorf("want:%#v got:%#v", v.want, res)
            }
        })

    }
}

待测函数如下:

package main

import (
    "fmt"
    "strings"
)

func subStr(str, stre string) []string {
    index := strings.Index(str, stre)
    var theSub []string

    for index >= 0 {
        // 先将分隔符前面的数据进行保存
        temp := str[:index]
        // 将字符串往后移
        str = str[index+len(stre):]
        // 重新获取下标
        index = strings.Index(str, stre)
        if temp != "" {
            theSub = append(theSub, temp)
        } else {
            continue
        }
    }
    theSub = append(theSub, str)
    return theSub[:]
}

func main() {
    fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":c"))
    fmt.Printf("%#v\n", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
    fmt.Println(123)
}

(1).组测试结果分析及命令:

  • 组测试命令仍使用 go test

--- FAIL: TestSubStr (0.00s)
    --- FAIL: TestSubStr/case_1 (0.00s)
       subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}        
    --- FAIL: TestSubStr/case_2 (0.00s)
         subStr_test.go:46: want:[]string{"1", "eqwe", "1@23"} got:[]string{"eqwe", "1@23"}
    --- FAIL: TestSubStr/case_3 (0.00s)
         subStr_test.go:46: want:[]string{"12@3", "@eq@we", "1@23"} got:[]string{"2@3", "@eq@we", "1@23"}
    --- FAIL: TestSubStr/case_5 (0.00s)
         subStr_test.go:46: want:[]string{"1", "3:eqwe:1", "3"} got:[]string{"3:eqwe:1", "3"}
    FAIL
    exit status 1
    FAIL    _/D_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/组测试与子测试     0.155s

(2).子测试结果分析及命令:

  • 例如单独测试case_1,使用的命令为go test -v -run=TestSubStr/case_1(等号后面跟的是上面组测试失败案例FAIL后面的目录)

=== RUN   TestSubStr
=== RUN   TestSubStr/case_1
    subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}
--- FAIL: TestSubStr (0.00s)
    --- FAIL: TestSubStr/case_1 (0.00s)
FAIL
exit status 1
FAIL    _/D_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/组测试与子测试     0.186s

三、pprof调试工具

1.对主函数进行传参

(1)os.Args
package main1

import (
    "fmt"
    "os"
)

func main() {
    // os.Args可以在执行函数的时候传递参数,但是对于-name=xxx没有办法将其解析
    if os.Args != nil {
        for index, temp := range os.Args {
            fmt.Println("第", index+1, "个参数是:", temp)
        }
    }
    fmt.Println("hello")
}

(2)flag.Args

  •    相对于os.Args来说,flag.Args使用起来更加方便
package main

import (
    "flag"
    "fmt"
    "time"
)

func main() {
    // 第一个参数为对应的参数,第二个参数为默认值,第三个参数为提示
    // 返回的是对应类型的指针
    // name := flag.String("name", "Tom", "输入name")
    // sex := flag.Bool("sex", true, "是不是男性")
    // age := flag.Int("age", 10, "年龄")
    // flag.Parse()
    // fmt.Println(*name, *sex, *age)
    var name string
    var age int
    var sex bool
    flag.StringVar(&name, "name", "Tom", "输入name")
    flag.BoolVar(&sex, "sex", true, "是不是男性")
    flag.IntVar(&age, "age", 10, "年龄")
    tim := flag.Duration("time", time.Hour, "时间")
    // 将输入的数据进行解析,不使用这句话的话不能获取到name sex age等属性对应的值
    flag.Parse()
    fmt.Println(name, sex, age, *tim)
    fmt.Println(flag.Args())  //以切片的方式返回命令行之外的其他参数
    fmt.Println(flag.NArg())  //返回命令行之外的其他参数的个数
    fmt.Println(flag.NFlag()) //返回使用命令行的参数的个数
}

2.pprof性能调优

pprof调试工具进行调试,主要看待测模块在内存于时间上的效益调试的时候只会显示耗费时间空间较多的代码段.

生成调试代码块的文件:go run xx.exe -cpu....
使用go语言工具查看代码块所存在的问题:go tool pprof cpu.pprof

package main

import (
    "flag"
    "fmt"
    "os"
    "runtime/pprof"
    "time"
)
// 一段有问题的代码
func logicCode() {
    var c chan int
    for {
        select {
        case v := <-c:
            fmt.Printf("recv from chan, value:%v\n", v)
        default:

        }
    }
}

func main() {
    var isCPUPprof bool
    var isMemPprof bool
    //获取接收到的参数
    flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
    flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
    flag.Parse()
    //判断进行什么测试,然后执行分支,并将信息保存到相应的文件内。
    if isCPUPprof {
        file, err := os.Create("./cpu.pprof")
        if err != nil {
            fmt.Printf("create cpu pprof failed, err:%v\n", err)
            return
        }
        pprof.StartCPUProfile(file)
        defer pprof.StopCPUProfile()
    }
    for i := 0; i < 8; i++ {
        go logicCode()
    }
    time.Sleep(20 * time.Second)
    if isMemPprof {
        file, err := os.Create("./mem.pprof")
        if err != nil {
            fmt.Printf("create mem pprof failed, err:%v\n", err)
            return
        }
        pprof.WriteHeapProfile(file)
        file.Close()
    }
}

总结:

这里对常用的测试函数,子测试进行了详细的讲解,对于pprof参数调优仅仅做了个介绍,可以根据自己的兴趣在网上找一部分pprof的例子深入学习,进行自动化测试非常方便。

到此这篇关于Go语言单元测试超详细解析的文章就介绍到这了,更多相关Go语言单元测试内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 用go语言实现WebAssembly数据加密的示例讲解

    用go语言实现WebAssembly数据加密的示例讲解

    在Web开发中,有时候为了提升安全性需要对数据进行加密,由于js代码相对比较易读,直接在js中做加密安全性较低,而WebAssembly代码不如js易读,本文提供一个用go语言实现的WebAssembly数据加密示例,需要的朋友可以参考下
    2024-03-03
  • golang官方嵌入文件到可执行程序的示例详解

    golang官方嵌入文件到可执行程序的示例详解

    这篇文章主要介绍了golang官方嵌入文件到可执行程序,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • Go-ethereum 解析ethersjs中产生的签名信息思路详解

    Go-ethereum 解析ethersjs中产生的签名信息思路详解

    这篇文章主要介绍了Go-ethereum 解析ethersjs中产生的签名信息,我们解析签名的需要知道,签名的消息,签名,和公钥,按照这个思路,我们可以通过ethers实现消息的签名,也可以通过go-ethereum实现,需要的朋友可以参考下
    2022-08-08
  • 一文带你掌握go中的异常处理

    一文带你掌握go中的异常处理

    代码在执行的过程中可能因为一些逻辑上的问题而出现错误,我们称之为异常,那么如何处理这些异常呢,下面小编就来和大家详细聊聊golang中的异常处理机制吧
    2023-09-09
  • 提升Golang应用性能:深入理解Context的应用

    提升Golang应用性能:深入理解Context的应用

    本文将深入探讨如何通过深入理解和正确应用Go语言中的Context来提升应用性能。需要的朋友可以参考下
    2023-09-09
  • golang中字符串和数字转换方法

    golang中字符串和数字转换方法

    在Golang中,可以使用strconv包中的Itoa()和Atoi()函数进行字符串与数字之间的转换,Itoa()用于将数字转换为字符串,Atoi()则用于将字符串转换回数字,本文介绍golang中字符串和数字转换方法,感兴趣的朋友一起看看吧
    2024-09-09
  • Go语言之使用pprof工具查找goroutine(协程)泄漏

    Go语言之使用pprof工具查找goroutine(协程)泄漏

    这篇文章主要介绍了Go语言之使用pprof工具查找goroutine(协程)泄漏,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Golang官方限流器库实现限流示例详解

    Golang官方限流器库实现限流示例详解

    这篇文章主要为大家介绍了Golang官方限流器库使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Go如何优雅的关闭goroutine协程

    Go如何优雅的关闭goroutine协程

    本文将介绍首先为什么需要主动关闭goroutine,并介绍如何在Go语言中关闭goroutine的常见套路,包括传递终止信号和协程内部捕捉终止信号,之后,文章列举了需要主动关闭协程运行的常见场景,希望通过本文的介绍,读者能够掌握如何在适当的时候关闭goroutine
    2023-05-05
  • Golang实现AES对称加密算法实例详解

    Golang实现AES对称加密算法实例详解

    所谓对称加密是指在加密和解码时使用同一密钥的加密方式,下面这篇文章主要给大家介绍了关于Golang实现AES对称加密算法的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-02-02

最新评论