持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
Hello World
Go 语言中通过约束判定一个文件或者方法为单元测试方法,具体的规则也很简单
- 文件以
xxx_test.gotest结尾的判定为是一个测试的源文件 - 测试的方法以
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
-
name参数为子测试的名字,f为子测试函数,本例中Run()一直阻塞到f执行结束后才返回,返回值为f的执行结果 -
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代表测试失败