golang进阶(七)——go语言的几种测试方法

前言

用了go的测试框架,再想下junit的,虽然已经Junit5,那丑陋程度还是依然。

java出来的时候,还没有很多软件工程的概念,语言先出来了,因此需要通过不同的插件慢慢补。

go就很幸运,出来的时候很多软件工程的概念已经基本定了下来,可以加到语言特性之中,go的测试就简便很多,不愧是为工程而生的语言

需要测试的程序

我们需要测试的程序文件叫做 utils.go ,里面有个字符串反转的方法 Reverse ,代码相对简单,这里就不赘述了

func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

单元测试

go的testing包可以基于包进测试,意思是执行 go test 默认单位是包范围, go test 可以自动执行当前包下面的所有 func TestXxx(*testing.T) 格式的测试方法,Xxx可以是任意字母,但最好和你需要测试方法一一对应。

新建一个测试的文件,要以 _test.go 作为文件名的结尾,最好和需要测试的文件一一对应,可以一目了然,这些test文件在程序构建的时候是不会一起打包到最终执行文件或者库中的。

我们新建 utlis_test.go 文件:

func TestReverse(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    cases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {"Hello, 世界", "界世 ,olleH"},
        {"", ""},
    }
    for _, c := range cases {
        got := Reverse(c.in)
        if got != c.want {
            t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
        }
    }
}

第一个if表示如果有 -short 参数,将会跳过这个测试,后面的代码相对就很容易看懂了,判断下输出输入是否一致,以上测试方法通过 go test 命令可以直接运行

示例验证

还有一种可以当做测试的方法就是使用示例验证程序,做一个示例在go语言也非常简单,告别写点示例程序就得写好多main方法的时代吧,格式要以Example开头,执行 go test 的时候会自动执行这些示例,要注意一点的是,Example之后需要跟已定义的变量,否则 vet 会报错。

还有一点注意的是,会自动trim,而忽略前后的空格比较。

func ExampleReverse() {
    fmt.Println(Reverse("Hello, 世界"))
    // Output: 界世 ,olleH
}

可以验证输出是否与想要的一致,还有一些情况下,比如多线程的情况下,输出顺序是随机的,这点go也考虑到了,

func ExampleReverse() {
    fmt.Println(Reverse("Hello, world"))
    fmt.Println(Reverse("Hello, 世界"))
    // Unordered Output: 界世 ,olleH
    // dlrow ,olleH
  // AA
}

Unordered Output 来表示无序输出

基准测试

基准测试以 Benchmark 为前缀,必须要执行 b.N 次,这样的测试才有对照性, b.N 的值是系统根据实际情况去调整的,从而保证测试的稳定性。

b.ResetTimer() 之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作

基准测试并不会默认执行,他需要增加 -bench 参数,例如 go test -bench .

func BenchmarkReverse(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Reverse("s string")
    }
}

基准测试也可以开启并行测试,需要执行 b.RunParallel(func(pb *testing.PB) 方法,默认会以逻辑CPU个数来进行并行测试。

个人意见只写并行测试就ok了,如果想非并行可以指定cpu数量为1,例如 go test -bench . -cpu 1

func BenchmarkReverseParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Reverse("s string")
        }
    })
}

子测试和子基准测试

还可以通过 TB 的Run方法开启子测试和子基准测试,主要是可以共享公共的设置和资源清除的管理

每个子测试都有一个唯一的名字,以父测试用 / 隔开来唯一表示,运行的时候使用 -run regexp 指定测试和 -bench regexp 来指定基准测试, . 表示所有。

还有一个特点就是所有子测试完成,父测试才算完成,而且所有测试都是并行的,这样可以把一些需要同步完成的操作来进行分组测试。

func TestTeardownParallel(t *testing.T) {
    // This Run will not return until the parallel tests finish.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", parallelTest1)
        t.Run("Test2", parallelTest2)
        t.Run("Test3", parallelTest3)
    })
    // <tear-down code>
}

Main测试

还有一个可以做一些初始化的地方就是main测试了,代码也相对简单。

要注意的testmain是在主协程运行的, m.Run() 开始运行时,其他测试才会执行

func TestMain(m *testing.M) {
    fmt.Println("init")
    os.Exit(m.Run())
}

附带揭露一下滴滴的jsoniter

为了kpi连基准测试都要作弊吗?

可见自己会写基准测试很重要,不会被人忽悠瘸了

来看我的基准测试

type ColorGroup struct {
    ID     int
    Name   string
    Colors []string
}

var group = ColorGroup{
    ID:     1,
    Name:   "Reds",
    Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}

func BenchmarkStdJson(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            b, err := json.Marshal(group)
            if err != nil {
                fmt.Println(b)
            }
        }
    })
}

func BenchmarkIterJson(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            b, err := jsoniter.Marshal(group)
            if err != nil {
                fmt.Println(b)
            }
        }
    })
}

测试结果:

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章