go test详解

使用Go自身的测试系统使用起来非常简单,只需要添加很少的代码就可以快速开始测试。

目前Go测试系统支持单元测试、性能测试和示例测试。

 

单元测试

单元测试是指对软件中的最小可测试单元进行检查和验证,比如对一个函数的测试。

 

性能测试

性能测试,也称基准测试,可以测试一段程序的性能,可以得到时间消耗、内存使用情况的报告。

 

示例测试

示例测试,广泛应用于Go源码和各种开源框架中,用于展示某个包或某个方法的用法。

比如,Go标准库中,mail包展示如何从一个字符串解析出邮件列表的用法,非常直观易懂。

源码位于src/net/mail/example_test.go中:

func ExampleParseAddressList() {

    const list = "Alice <[email protected]>, Bob <[email protected]>, Eve <[email protected]>"

    emails, err := mail.ParseAddressList(list)

    if err != nil {

        log.Fatal(err)

    }

    for _, v := range emails {

        fmt.Println(v.Name, v.Address)

    }

    // Output:

    // Alice [email protected]
    
    // Bob [email protected]

    // Eve [email protected]

}

本节,我们通过简单的例子,快速体验一下如何使用Go的测试系统进行测试。

1 单元测试

源代码目录结构

我们在gotest包中创建两个文件,目录结构如下所示:

[GoExpert]

|--[src]

|--[gotest]

|--unit.go

|--unit_test.go

其中unit.go为源代码文件,unit_test.go为测试文件。要保证测试文件以“_test.go”结尾。

 

源代码文件

源代码文件unit.go中包含一个Add()方法,如下所示:

package gotest

​

// Add 方法用于演示go test使用

func Add(a int, b int) int {

    return a + b

}

Add()方法仅提供两数加法,实际项目中不可能出现类似的方法,此处仅供单元测试示例。

 

测试文件

测试文件unit_test.go中包含一个测试方法TestAdd(),如下所示:

package gotest_test

​

import (

    "testing"

    "gotest"

)

​

func TestAdd(t *testing.T) {

    var a = 1

    var b = 2

    var expected = 3

​

    actual := gotest.Add(a, b)

    if actual != expected {

        t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)

    }

}

通过package语句可以看到,测试文件属于“gotest_test”包,测试文件也可以跟源文件在同一个包,但常见的做法是创建一个包专用于测试,这样可以使测试文件和源文件隔离。GO源代码以及其他知名的开源框架通常会创建测试包,而且规则是在原包名上加上"_test"。

测试函数命名规则为"TestXxx",其中“Test”为单元测试的固定开头,go test只会执行以此为开头的方法。紧跟“Test”是以首字母大写的单词,用于识别待测试函数。

测试函数参数并不是必须要使用的,但"testing.T"提供了丰富的方法帮助控制测试流程。

t.Errorf()用于标记测试失败,标记失败还有几个方法,在介绍testing.T结构时再详细介绍。

 

执行测试

命令行下,使用go test命令即可启动单元测试,如下所示:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test

PASS

ok gotest 0.378s

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>

通过打印可知,测试通过,花费时间为0.378s。

 

总结

从上面可以看出,编写一个单元测试并执行是非常方便的,只需要遵循一定的规则:

  • 测试文件名必须以"_test.go"结尾;

  • 测试函数名必须以“TestXxx”开始;

  • 命令行下使用"go test"即可启动测试;

2 性能测试

源代码目录结构

我们在gotest包中创建两个文件,目录结构如下所示:

[GoExpert]

|--[src]

|--[gotest]

|--benchmark.go

|--benchmark_test.go

其中benchmark.go为源代码文件,benchmark_test.go为测试文件。

 

源代码文件

源代码文件benchmark.go中包含MakeSliceWithoutAlloc()MakeSliceWithPreAlloc()两个方法,如下所示:

package gotest

​

// MakeSliceWithPreAlloc 不预分配

func MakeSliceWithoutAlloc() []int {

    var newSlice []int

​

    for i := 0; i < 100000; i++ {

        newSlice = append(newSlice, i)

    }

​

    return newSlice

}

​

// MakeSliceWithPreAlloc 通过预分配Slice的存储空间构造

func MakeSliceWithPreAlloc() []int {

    var newSlice []int

​

    newSlice = make([]int, 0, 100000)

    for i := 0; i < 100000; i++ {

        newSlice = append(newSlice, i)

    }

​

    return newSlice

}

两个方法都会构造一个容量为100000的切片,所不同的是MakeSliceWithPreAlloc()会预先分配内存,而MakeSliceWithoutAlloc()不预先分配内存,二者理论上存在性能差异,本次就来测试一下二者的性能差异。

 

测试文件

测试文件benchmark_test.go中包含两个测试方法,用于测试源代码中两个方法的性能,测试文件如下所示:

package gotest_test

​

import (

    "testing"

    "gotest"

)

​

func BenchmarkMakeSliceWithoutAlloc(b *testing.B) {

    for i := 0; i < b.N; i++ {

        gotest.MakeSliceWithoutAlloc()

    }

}

​

func BenchmarkMakeSliceWithPreAlloc(b *testing.B) {

    for i := 0; i < b.N; i++ {

        gotest.MakeSliceWithPreAlloc()

    }

}

性能测试函数命名规则为"BenchmarkXxx",其中"Xxx"为自定义的标识,需要以大写字母开始,通常为待测函数。

testing.B提供了一系列的用于辅助性能测试的方法或成员,比如本例中的b.N表示循环执行的次数,而N值不用程序员特别关心,按照官方说法,N值是动态调整的,直到可靠的算出程序执行时间后才会停止,具体执行次数会在执行结束后打印出来。

 

执行测试

命令行下,使用go test -bench=.命令即可启动性能测试,如下所示:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test -bench=.

BenchmarkMakeSliceWithoutAlloc-4 2000 1103822 ns/op

BenchmarkMakeSliceWithPreAlloc-4 5000 328944 ns/op

PASS

ok gotest 4.445s

其中-bench为go test的flag,该flag指示go test进行性能测试,即执行测试文件中符合"BenchmarkXxx"规则的方法。 紧跟flag的为flag的参数,本例表示执行当前所有的性能测试。

通过输出可以直观的看出,BenchmarkMakeSliceWithoutAlloc执行了2000次,平均每次1103822纳秒,BenchmarkMakeSliceWithPreAlloc执行了5000次,平均每次328944纳秒。

从测试结果上看,虽然构造切片很快,但通过给切片预分配内存,性能还可以进一步提升,符合预期。关于原理分析,请参考Slice相关章节。

 

总结

从上面的例子可以看出,编写并执行性能测试是非常简单的,只需要遵循一些规则:

  • 文件名必须以“_test.go”结尾;

  • 函数名必须以“BenchmarkXxx”开始;

  • 使用命令“go test -bench=.”即可开始性能测试;

3 示例测试

源代码目录结构

我们在gotest包中创建两个文件,目录结构如下所示:

[GoExpert]

|--[src]

|--[gotest]

|--example.go

|--example_test.go

其中example.go为源代码文件,example_test.go为测试文件。

 

源代码文件

源代码文件example.go中包含SayHello()SayGoodbye()PrintNames()三个方法,如下所示:

package gotest


import "fmt"


// SayHello 打印一行字符串

func SayHello() {

    fmt.Println("Hello World")

}

​

// SayGoodbye 打印两行字符串

func SayGoodbye() {

    fmt.Println("Hello,")

    fmt.Println("goodbye")

}

​

// PrintNames 打印学生姓名

func PrintNames() {

    students := make(map[int]string, 4)

    students[1] = "Jim"

    students[2] = "Bob"

    students[3] = "Tom"

    students[4] = "Sue"

    for _, value := range students {

        fmt.Println(value)

    }

}

这几个方法打印内容略有不同,分别代表一种典型的场景:

  • SayHello():只有一行打印输出

  • SayGoodbye():有两行打印输出

  • PrintNames():有多行打印输出,且由于Map数据结构的原因,多行打印次序是随机的。

 

测试文件

测试文件example_test.go中包含3个测试方法,于源代码文件中的3个方法一一对应,测试文件如下所示:

package gotest_test

​

import "gotest"

​

// 检测单行输出

func ExampleSayHello() {

    gotest.SayHello()

    // OutPut: Hello World

}

​

// 检测多行输出

func ExampleSayGoodbye() {

    gotest.SayGoodbye()

    // OutPut:

    // Hello,

    // goodbye

}

​

// 检测乱序输出

func ExamplePrintNames() {

    gotest.PrintNames()

    // Unordered output:

    // Jim

    // Bob

    // Tom

    // Sue

}

例子测试函数命名规则为"Examplexxx",其中"xxx"为自定义的标识,通常为待测函数名称。

这三个测试函数分别代表三种场景:

  • ExampleSayHello(): 待测试函数只有一行输出,使用"// OutPut: "检测。

  • ExampleSayGoodbye():待测试函数有多行输出,使用"// OutPut: "检测,其中期望值也是多行。

  • ExamplePrintNames():待测试函数有多行输出,但输出次序不确定,使用"// Unordered output:"检测。

注:字符串比较时会忽略前后的空白字符。

 

执行测试

命令行下,使用go testgo test example_test.go命令即可启动测试,如下所示:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test example_test.go

ok command-line-arguments 0.331s

 

总结

  1. 例子测试函数名需要以"Example"开头;

  2. 检测单行输出格式为“// Output: <期望字符串>”;

  3. 检测多行输出格式为“// Output: <期望字符串> <期望字符串>”,每个期望字符串占一行;

  4. 检测无序输出格式为"// Unordered output: <期望字符串> <期望字符串>",每个期望字符串占一行;

  5. 测试字符串时会自动忽略字符串前后的空白字符;

  6. 如果测试函数中没有“Output”标识,则该测试函数不会被执行;

  7. 执行测试可以使用go test,此时该目录下的其他测试文件也会一并执行;

  8. 执行测试可以使用go test <xxx_test.go>,此时仅执行特定文件中的测试函数;

4 子测试

简介

简单的说,子测试提供一种在一个测试函数中执行多个测试的能力,比如原来有TestA、TestB和TestC三个测试函数,每个测试函数执行开始都需要做些相同的初始化工作,那么可以利用子测试将这三个测试合并到一个测试中,这样初始化工作只需要做一次。

除此之外,子测试还提供了诸多便利,下面我们逐一说明。

 

简单例子

我们先看一个简单的例子,以便快速了解子测试的基本用法。

package gotest_test

​

import (

    "testing"

    "gotest"

)

​

// sub1 为子测试,只做加法测试

func sub1(t *testing.T) {

    var a = 1

    var b = 2

    var expected = 3

​

    actual := gotest.Add(a, b)

    if actual != expected {

        t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)

    }

}

​

// sub2 为子测试,只做加法测试

func sub2(t *testing.T) {

    var a = 1

    var b = 2

    var expected = 3

​

    actual := gotest.Add(a, b)

    if actual != expected {

        t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)

    }

}

​

// sub3 为子测试,只做加法测试

func sub3(t *testing.T) {

    var a = 1

    var b = 2

    var expected = 3

​

    actual := gotest.Add(a, b)

    if actual != expected {

        t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)

    }

}

​

// TestSub 内部调用sub1、sub2和sub3三个子测试

func TestSub(t *testing.T) {

    // setup code

    t.Run("A=1", sub1)

    t.Run("A=2", sub2)

    t.Run("B=1", sub3)

​

    // tear-down code

}

本例中TestSub()通过t.Run()依次执行三个子测试。t.Run()函数声明如下:

func (t *T) Run(name string, f func(t *T)) bool

name参数为子测试的名字,f为子测试函数,本例中Run()一直阻塞到f执行结束后才返回,返回值为f的执行结果。 Run()会启动新的协程来执行f,并阻塞等待f执行结束才返回,除非f中使用t.Parallel()设置子测试为并发。

本例中TestSub()把三个子测试合并起来,可以共享setup和tear-down部分的代码。

我们在命令行下,使用-v参数执行测试:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v

=== RUN TestSub

=== RUN TestSub/A=1

=== RUN TestSub/A=2

=== RUN TestSub/B=1

--- PASS: TestSub (0.00s)

--- PASS: TestSub/A=1 (0.00s)

--- PASS: TestSub/A=2 (0.00s)

--- PASS: TestSub/B=1 (0.00s)

PASS

ok command-line-arguments 0.354s

从输出中可以看出,三个子测试都被执行到了,而且执行次序与调用次序一致。

 

子测试命名规则

通过上面的例子我们知道Run()方法第一个参数为子测试的名字,而实际上子测试的内部命名规则为:"<父测试名字>/<传递给Run的名字>"。比如,传递给Run()的名字是“A=1”,那么子测试名字为“TestSub/A=1”。这个在上面的命令行输出中也可以看出。

 

过滤筛选

通过测试的名字,可以在执行中过滤掉一部分测试。

比如,只执行上例中“A=*”的子测试,那么执行时使用-run Sub/A=参数即可:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v -run Sub/A=

=== RUN TestSub

=== RUN TestSub/A=1

=== RUN TestSub/A=2

--- PASS: TestSub (0.00s)

--- PASS: TestSub/A=1 (0.00s)

--- PASS: TestSub/A=2 (0.00s)

PASS

ok command-line-arguments 0.340s

上例中,使用参数-run Sub/A=则只会执行TestSub/A=1TestSub/A=2两个子测试。

对于子性能测试则使用-bench参数来筛选,此处不再赘述。

注意:此处的筛选不是严格的正则匹配,而是包含匹配。比如,-run A=那么所有测试(含子测试)的名字中如果包含“A=”则会被选中执行。

 

子测试并发

前面提到的多个子测试共享setup和teardown有一个前提是子测试没有并发,如果子测试使用t.Parallel()指定并发,那么就没办法共享teardown了,因为执行顺序很可能是setup->子测试1->teardown->子测试2...。

如果子测试可能并发,则可以把子测试通过Run()再嵌套一层,Run()可以保证其下的所有子测试执行结束后再返回。

为便于说明,我们创建文件subparallel_test.go用于说明:

package gotest_test

​

import (

    "testing"

    "time"

)

​

// 并发子测试,无实际测试工作,仅用于演示

func parallelTest1(t *testing.T) {

    t.Parallel()

    time.Sleep(3 * time.Second)

    // do some testing

}

​

// 并发子测试,无实际测试工作,仅用于演示

func parallelTest2(t *testing.T) {

    t.Parallel()

    time.Sleep(2 * time.Second)

    // do some testing

}

​

// 并发子测试,无实际测试工作,仅用于演示

func parallelTest3(t *testing.T) {

    t.Parallel()

    time.Sleep(1 * time.Second)

    // do some testing

}

​

// TestSubParallel 通过把多个子测试放到一个组中并发执行,同时多个子测试可以共享setup和tear-down

func TestSubParallel(t *testing.T) {

    // setup

    t.Logf("Setup")

​

    t.Run("group", func(t *testing.T) {

        t.Run("Test1", parallelTest1)

        t.Run("Test2", parallelTest2)

        t.Run("Test3", parallelTest3)

    })

​

    // tear down

    t.Logf("teardown")

}

上面三个子测试中分别sleep了3s、2s、1s用于观察并发执行顺序。通过Run()将多个子测试“封装”到一个组中,可以保证所有子测试全部执行结束后再执行tear-down。

命令行下的输出如下:

E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subparallel_test.go -v -run SubParallel

=== RUN TestSubParallel

=== RUN TestSubParallel/group

=== RUN TestSubParallel/group/Test1

=== RUN TestSubParallel/group/Test2

=== RUN TestSubParallel/group/Test3

--- PASS: TestSubParallel (3.01s)

subparallel_test.go:25: Setup

--- PASS: TestSubParallel/group (0.00s)

--- PASS: TestSubParallel/group/Test3 (1.00s)

--- PASS: TestSubParallel/group/Test2 (2.01s)

--- PASS: TestSubParallel/group/Test1 (3.01s)

subparallel_test.go:34: teardown

PASS

ok command-line-arguments 3.353s

通过该输出可以看出: 1. 子测试是并发执行的(Test1最先被执行却最后结束) 2. tear-down在所有子测试结束后才执行

 

总结

  • 子测试适用于单元测试和性能测试;

  • 子测试可以控制并发;

  • 子测试提供一种类似table-driven风格的测试;

  • 子测试可以共享setup和tear-down;

5 Main测试

简介

我们知道子测试的一个方便之处在于可以让多个测试共享Setup和Tear-down。但这种程度的共享有时并不满足需求,有时希望在整个测试程序做一些全局的setup和Tear-down,这时就需要Main测试了。

所谓Main测试,即声明一个func TestMain(m *testing.M),它是名字比较特殊的测试,参数类型为testing.M指针。如果声明了这样一个函数,当前测试程序将不是直接执行各项测试,而是将测试交给TestMain调度。

示例

下面通过一个例子来展示Main测试用法:

// TestMain 用于主动执行各种测试,可以测试前后做setup和tear-down操作

func TestMain(m *testing.M) {

println("TestMain setup.")

retCode := m.Run() // 执行测试,包括单元测试、性能测试和示例测试

println("TestMain tear-down.")

os.Exit(retCode)

}

上述例子中,日志打印的两行分别对应Setup和Tear-down代码,m.Run()即为执行所有的测试,m.Run()的返回结果通过os.Exit()返回。

如果所有测试均通过测试,m.Run()返回0,否同m.Run()返回1,代表测试失败。

有一点需要注意的是,TestMain执行时,命令行参数还未解析,如果测试程序需要依赖参数,可以使用flag.Parse()解析参数,m.Run()方法内部还会再次解析参数,此处解析不会影响原测试过程。

6 httptest

前面介绍了go test用于单元测试、性能测试和示例测试,但Web应用程序中往往需要与其他系统进行交互,比如通过http访问其他系统,此时就需要有一种方法用于打桩来模拟Web服务器和客户端,httptest包即Go语言针对Web应用提供的解决方案。

httptest可以方便的模拟各种Web服务器和客户端,以达到测试目的。

发布了76 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43778179/article/details/104797682