Go 单元测试(一)

324 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

Hello World

Go 语言中通过约束判定一个文件或者方法为单元测试方法,具体的规则也很简单

  1. 文件以xxx_test.go test结尾的判定为是一个测试的源文件
  2. 测试的方法以Testxxx开头的方法名,并且方法的签名为func(testing.T)或者func(testing.B)的方法,判定为一个单元测试的方法
func TestHelloWorld(t *testing.T) {
	t.Log("hello go test")
}

中终端执行go test -v -run TestHelloWorld,执行测试方法

$ go test -v -run TestHelloWorld 
=== RUN   TestHelloWorld
    hello_test.go:6: hello go test
--- PASS: TestHelloWorld (0.00s)
PASS
ok      gotest  0.420s

单元测试

这里使用 Add函数作为测试的方法

func Add(a, b int) int {
    return a + b
}
func TestAdd(t *testing.T) {
	a := 1
	b := 1
	expected := 2
	actual := Add(a, b)

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

执行 go test -v -run TestAdd

% go test -v -run TestAdd
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      gotest  0.291s

性能测试

func MakeSliceWithoutAlloc(size int) []int {
	var newSlice []int
	for i := 0; i < size; i++ {
		newSlice = append(newSlice, i)
	}
	return newSlice
}

func MakeSliceWitPrevAlloc(size int) []int {
	newSlice := make([]int, 0, size)
	for i := 0; i < size; i++ {
		newSlice = append(newSlice, i)
	}
	return newSlice
}

两个方法都是给一个整形的切片数组赋值,一个是提前申请好容量,另一个是通过Go语言的动态扩容机制在运行时动态扩容。

编写性能测试方法

这里请注意,测试方法传入的入参是*testing.B,并且压测的方法名称要以BenchmarkXXX开头

var size = 10000

func BenchmarkMakeSliceWithoutAlloc(b *testing.B) {
	for i := 0; i < b.N; i++ {
		MakeSliceWithoutAlloc(size)
	}
}

func BenchmarkMakeSliceWithPreAlloc(b *testing.B) {
	for i := 0; i < b.N; i++ {
		MakeSliceWithoutAlloc(size)
	}
}

通过运行go test -bench=.进行压力测试

$ go test -bench=.
goos: darwin
goarch: amd64
pkg: gotest
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkMakeSliceWithoutAlloc-12          23202             51690 ns/op
BenchmarkMakeSliceWithPreAlloc-12          27728             40999 ns/op
PASS
ok      gotest  6.598s

通过测试结果可以看出执行测试的CPU信息,系统架构等一些基本信息。

通过输出可以直观的看出,BenchmarkMakeSliceWithoutAlloc执行了23202次,平均每次51690纳秒,BenchmarkMakeSliceWithPreAlloc执行了27728次,平均每次40999纳秒。通过测试结果可以得出,预先分配切片大小的性能比较好~

示例测试

测试输出的内容为指定的信息

func SayHello() {
	fmt.Println("hello World")
}

func SayGoodBye() {
	fmt.Println("hello")
	fmt.Println("good bye")
}

func SayName() {
	nameMap := map[int]string{
		1: "Tom",
		2: "Jim",
		3: "Kitty",
		4: "Erik",
	}

	for id, name := range nameMap {
		fmt.Printf("%d::%s\n", id, name)
	}
}

SayHello的输出信息为hello World SayGoodBye的输出信息为hello \n good bye SayName的输出的信息不确定输出内容的顺序

对应的测试方法如下:

这里值得注意的是测试示例输出的方法名称为 ExampleXXX

func ExampleSayHello() {
	SayHello()
	// Output: hello World
}

func ExampleSayGoodBye() {
	SayGoodBye()
	// Output:
	// hello
	// good bye
}

func ExampleSayName() {
	SayName()
	// Unordered output:
	// 2::Jim
	// 3::Kitty
	// 4::Erik
	// 1::Tom
}

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

  • ExampleSayHello: 待测试函数只有一行输出,使用”// OutPut: “检测。
  • ExampleSayGoodbye:待测试函数有多行输出,使用”// OutPut: “检测,其中期望值也是多行。
  • ExamplePrintNames:待测试函数有多行输出,但输出次序不确定,使用”// Unordered output:”检测。

通过运行go test 测试文件名称运行测试,我这里的测试文件名称为example_test.go

$ go test example_test.go

=== RUN   ExampleSayHello
--- PASS: ExampleSayHello (0.00s)
=== RUN   ExampleSayGoodBye
--- PASS: ExampleSayGoodBye (0.00s)
=== RUN   ExampleSayName
--- PASS: ExampleSayName (0.00s)
PASS
ok      gotest  0.298s

子测试

自测试是指在一个测试方法中测试多个测试方法的能力,比如我们在测试一些方法的时候需要做一些初始化工作,测试完成后,要对一些资源进行回收,那么我们可以使用子测试,将这些需要相同初始化工作的测试集成为一个测试。

如下面这个例子


func sub1(t *testing.T) {
	t.Log("sub1")
	a := 1
	b := 2
	expect := 3
	actual := Add(a, b)
	if actual != expect {
		t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expect)
	}
}

func sub2(t *testing.T) {
	t.Log("sub2")
	a := 1
	b := 2
	expect := 3
	actual := Add(a, b)
	if actual != expect {
		t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expect)
	}
}

func sub3(t *testing.T) {
	t.Log("sub3")
	a := 1
	b := 2
	expect := 3
	actual := Add(a, b)
	if actual != expect {
		t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expect)
	}
}

func TestSub(t *testing.T) {
   // todo test pre work
   
	t.Run("1", sub1)
	t.Run("2", sub2)
	t.Run("3", sub3)
        
  // todo test end work
}

t.Run的函数签名是:func Run(name string, f func(t *T)) bool

  1. name参数为子测试的名字,f为子测试函数,本例中Run()一直阻塞到f执行结束后才返回,返回值为f的执行结果

  2. Run()会启动新的协程来执行f,并阻塞等待f执行结束才返回,除非f中使用t.Parallel()设置子测试为并发

通过执行go test -run ^TestSub$


go test -run ^TestSub$

=== RUN   TestSub
=== RUN   TestSub/1
=== RUN   TestSub/2
=== RUN   TestSub/3
--- PASS: TestSub (0.00s)
    --- PASS: TestSub/1 (0.00s)
    --- PASS: TestSub/2 (0.00s)
    --- PASS: TestSub/3 (0.00s)
PASS
ok      gotest  0.098s
子测试命名规则

我们传给Run方法的自测试名称虽然是1,2,3,但是实际子测试的名称是<父名称>/<子名称>

执行指定的子测试方法

通过命令传入子测试名称即可,如,执行TestSub/2子测试,

go test -run TestSub/2

方法名称支持前缀筛选,比如,需要测试以TestSub/开头的所有子测试,那么可以这样写

go test -run TestSub/

子测试并发

上文中提到的子测试是串行执行,如果我们想要并行执行子测试用例,我们可以在每个子测试的方法中通过加入t.Parallel()开启并行测试。但是上文中的测试要开始子测试的话,会有一点问题,因为子测试的执行顺序是不确定的,有可能是 pre work -> sub1 -> end work -> sub2... 也就是说任何一个子测试执行结束都会执行end work,这样就会影响其他子测试的运行,解决这种问题的方法我们可以使用t.Run对测试再进行一次包装,这样就不会互相影响了。

func parallelTest1(t *testing.T) {
	t.Parallel()
	time.Sleep(time.Second)
	t.Log("parallel test1 finish")
}

func parallelTest2(t *testing.T) {
	t.Parallel()
	time.Sleep(2 * time.Second)
	t.Log("parallel test2 finish")
}

func parallelTest3(t *testing.T) {
	t.Parallel()
	time.Sleep(3 * time.Second)
	t.Log("parallel test1 finish")
}

func TestSubParallel(t *testing.T) {
	t.Run("sub parallel", func(t *testing.T) {
		t.Run("pt1", parallelTest1)
		t.Run("pt2", parallelTest2)
		t.Run("pt3", parallelTest3)
	})
	t.Log("finish sub parallel test")
}

通过运行 go test -run ^TestSubParallel$

go test -run ^TestSubParallel$ 

=== RUN   TestSubParallel
=== RUN   TestSubParallel/sub_parallel
=== RUN   TestSubParallel/sub_parallel/pt1
=== PAUSE TestSubParallel/sub_parallel/pt1
=== RUN   TestSubParallel/sub_parallel/pt2
=== PAUSE TestSubParallel/sub_parallel/pt2
=== RUN   TestSubParallel/sub_parallel/pt3
=== PAUSE TestSubParallel/sub_parallel/pt3
=== CONT  TestSubParallel/sub_parallel/pt1
=== CONT  TestSubParallel/sub_parallel/pt2
=== CONT  TestSubParallel/sub_parallel/pt3
=== CONT  TestSubParallel/sub_parallel/pt1
=== CONT  TestSubParallel/sub_parallel/pt2
=== CONT  TestSubParallel/sub_parallel/pt3
=== CONT  TestSubParallel
--- PASS: TestSubParallel (3.00s)
    --- PASS: TestSubParallel/sub_parallel (0.00s)
        --- PASS: TestSubParallel/sub_parallel/pt1 (1.00s)
        --- PASS: TestSubParallel/sub_parallel/pt2 (2.00s)
        --- PASS: TestSubParallel/sub_parallel/pt3 (3.00s)
PASS
ok      gotest  4.140s

可以看出,测试是并行的,并且测试的回收工作是等待所有的子测试完成在进行回收的。

Main测试

子测试是将一些测试进行了合并,并统一初始化和结束收尾的工作,但是在有些时候,我们并不想将一些测试进行合并,但是需要在测试前对一些资源进行初始化,并在测试结束之后,对资源进行相应的回收,这时,我们可以使用Go提供的Main测试

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

func TestMain(m *testing.M) {
    println("pre work")

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

    println("end work")

}

这里的返回值code等于0代表所有的测试通过,等于1代表测试失败