go语言测试框架

go语言内置的测试框架能够完成基本的功能测试,基准测试,和样本测试。

测试框架

  • go语言测试单元以包为单位组织,包含包里的一个或者多个测试文件。
  • 测试文件以_test.go结尾,通常放在待测代码相同目录下,即他们属于同一个包。
  • 测试用例以TestXxxx/ExampleXxxx/BenchmarkXxxx的格式组织在测试文件里。
    当然也可以在测试文件里面定义其他非上述格式的本地函数,以供调用。

功能测试: TestXxxx(t *testing.T)

功能测试偏向于测试源码文件中的具体功能,属于UT的范畴,比如具体的函数功能。

先举一个例子来说
假定定义了一个函数计算平均值:

$ cat myavg.go
package myavg

func MyAvg(a []float64) float64 {
  ...
}

下面来为这个函数增加测试用例

$ cat myavg_test.go
package myavg

import (
    "testing"
)

func TestMyAvg(t *testing.T) {
    a := []float64 {1, 2, 3, 4, 5}
    e := 3
    v := MyAvg(a)
    if v != 3 {
        t.Errorf("case (%v) expect (%v) but got (%v)", a, e, v)
    }
}

运行

$ go test -v
=== RUN   TestMyAvg
--- PASS: TestMyAvg (0.00s)
PASS
ok      myavg   0.008s

几点说明:

  • 测试文件是否与待测文件在相同目录下
    这并不是固定的;如果放在同一个目录下使用相同的包(package)名,方便当代码功能更改时,及时相应的更改测试代码;如果把测试文件单独组织在另一个包下面,例如myavg_test,也可以完成测试功能,例如:
$ cat myavg_test.go
package myavg_test

import (
    "myavg"

    "testing"
)

func TestMyAvg(t *testing.T) {
    a := []float64 {1, 2, 3, 4, 5}
    e := 3
    v := myavg.MyAvg(a)
    if v != 3 {
        t.Errorf("case (%v) expect (%v) but got (%v)", a, e, v)
    }
}

这个例子中我们把测试文件放在一个单独的包(myavg_test)里,区别就是必须import myavg包,然后调用MyAvg()的时候必须指定包名myavg,因为他们属于不同的包(package)了。

其实go只规定了一点,即以_test.go结尾的文件会认为是测试文件,在编译(go build)的时候会被忽略,只在测试(go test)的时候被使用,所以测试文件是否和待测文件放在相同包里还是不同包里,可以根据项目,或者使用习惯组织。

  • 测试用例名称TestXxxx
    注意第一个字母X不能是小写字母,在实际项目中,第一个字母一般是大写字母或者下划线(_),这样便于代码的阅读;其实也可以是数字,但这实在是可读性差点了,虽然语法上没有问题。例如
TestMyAvg   // OK
Test_MyAvg  // OK
Test_myAvg  // OK
Test1myAvg  // not suggested
TestmyAvg   // Fail

测试例子的改进
前面的测试例子TestMyAvg中,我们给了一个用例,这个例子我们可以进行适当改进,方便多个组合的测试。

package myavg

import (
    "testing"
)

type casepair struct {
    val []float64
    avg float64
}

func TestAverage2(t *testing.T) {
    var cases = []casepair {
        { []float64{1, 2},              1.5 },
        { []float64{1, 1, 1, 1, 1, 1},  1 },
        { []float64{1, -1},             0 },
    }

    for _, pair := range cases {
        v := MyAvg(pair.val)
        if v != pair.avg {
            t.Errorf("case (%v) expect (%v) but got (%v)", pair.val, pair.avg, v)
        }
    }
}

运行

$ go test -v
=== RUN   TestAverage2
--- PASS: TestAverage2 (0.00s)
PASS
ok      myavg   0.003s

以这种方式组织测试材料,在定义输入的时候就定义好了结果输出,便于测试用例的组织,以及将来的添加,删除,等修改维护。

样本测试: ExampleXxxx()

样本测试用来验证运行的输出内容是否与预期的一样。
样本测试的格式和功能测试的格式类似,只是例子以ExampleXxxx格式,然后在函数中以//Output的方式指明输出,例如

package myavg

import (
    "fmt"
)

func ExampleHello() {
    a := []float64 {1, 2, 3, 4, 5}
    v := MyAvg(a)
    fmt.Printf("%.1f", v)
    // Output: 3.0
}

type casepair struct {
    val []float64
    avg float64
}

func ExampleHello2() {
    var cases = []casepair {
        { []float64{1, 2},              1.5 },
        { []float64{1, 1, 1, 1, 1, 1},  1 },
        { []float64{1, -1},             0 },
    }

    for _, pair := range cases {
        v := MyAvg(pair.val)
        fmt.Printf("%.1f\n", v)
    }

    // Output:
    // 1.5
    // 1.0
    // 0.0
}

写在注释后面的Output定义了函数执行希望的输出内容,go测试框架会比较这些输出和实际的输出是否一致,来决定测试用例是否通过。另外样本测试并不需要import testing包,而只需要定义函数名格式为ExampleXxxx即可,testing包是功能测试必须的。
定义Output的格式比较复杂可以参考go语言文档,这里不细说了,例子只给出了最基本的Output用法。

其实仔细想想,功能测试和样本测试没啥区别,他们可以互相改造,只是内容比较,用户可以以程序的方式比较字符串内容,或者由测试框架来比较而已。所以实际场景下样本测试使用的并不多。
运行:

$ go test -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleHello2
--- PASS: ExampleHello2 (0.00s)
PASS
ok      myavg   0.003s

我们给一个没有通过反例看一下失败的场景是怎么样的:

package myavg

import (
    "fmt"
)

func ExampleHello3() {
    fmt.Println("Hello")
    // Output:
    // hello
}

$ go test -v
=== RUN   ExampleHello
--- FAIL: ExampleHello3 (0.00s)
got:
Hello
want:
hello
FAIL
exit status 1
FAIL    myavg   0.004s

测试用例里面希望的输出是hello,而实际的执行输出是Hello,第一个字母的大小写不一致,所有这个用例运行失败。

基准测试: BenchmarkXxxx(b *testing.B)

没有弄过,以后再更新。

测试Main函数: TestMain(m *testing.M)

测试的TestMain函数主要用来在运行测试用例之前做一些必要的setup以及之后的tearDown操作。注意是在所有的测试运行之前和之后,因此一个测试包里面只能定义一个TestMain函数,下面的例子:

package myavg

import (
    "os"
    "log"
    "flag"

    "testing"
)


var maxValue  *int    = flag.Int   ("max",   100,      "the maxinum buffer size")
var typeValue *string = flag.String("type", "average", "the default type value")

func TestMain(m *testing.M) {
    flag.Parse()
    log.Printf("option[max]=(%d)\n", *maxValue)
    log.Printf("option[type]=(%s)\n", *typeValue)

    setup(*maxValue, *typeValue)
    exitcode := m.Run() // run all cases
    tearDown()
    os.Exit(exitcode)
}


func setup(maxValue int, typeValue string) {
    log.Println("Entry of setup")
}

func tearDown() {
    log.Println("Exit of tearDown")
}

运行

$ go test -v -max 12 -type anytype
2017/11/03 21:59:57 option[max]=(12)
2017/11/03 21:59:57 option[type]=(anytype)
2017/11/03 21:59:57 Entry of setup
...
2017/11/03 21:59:57 Exit of tearDown
ok      myavg   0.004s

首先在TestMain分析命令行参数,我们定义了max和type两个参数,然后把参数传个setup()做必要的初始化,接着运行测试案例,最后退出之前调用tearDown()做必要的清除操作。

    原文作者:CodingCode
    原文地址: https://www.jianshu.com/p/7c0b4aa69f82
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞