简介
testing
是 Go 语言规范库自带的测试库。在 Go 语言中编写测试很简略,只须要遵循 Go 测试的几个约定,与编写失常的 Go 代码没有什么区别。Go 语言中有 3 种类型的测试:单元测试,性能测试,示例测试。上面顺次来介绍。
单元测试
单元测试又称为功能性测试,是为了测试函数、模块等代码的逻辑是否正确。接下来咱们编写一个库,用于将示意罗马数字的字符串和整数互转。罗马数字是由M/D/C/L/X/V/I
这几个字符依据肯定的规定组合起来示意一个正整数:
- M=1000,D=500,C=100,L=50,X=10,V=5,I=1;
- 只能示意 1-3999 范畴内的整数,不能示意 0 和正数,不能示意 4000 及以上的整数,不能示意分数和小数(当然有其余简单的规定来示意这些数字,这里暂不思考);
- 每个整数只有一种示意形式,个别状况下,连写的字符示意对应整数相加,例如
I=1
,II=2
,III=3
。然而,十位字符(I/X/C/M
)最多呈现 3 次,所以不能用IIII
示意 4,须要在V
右边增加一个I
(即IV
)来示意,不能用VIIII
示意 9,须要应用IX
代替。另外五位字符(V/L/D
)不能间断呈现 2 次,所以不能呈现VV
,须要用X
代替。
// roman.gopackage romanimport ( "bytes" "errors" "regexp")type romanNumPair struct { Roman string Num int}var ( romanNumParis []romanNumPair romanRegex *regexp.Regexp)var ( ErrOutOfRange = errors.New("out of range") ErrInvalidRoman = errors.New("invalid roman"))func init() { romanNumParis = []romanNumPair{ {"M", 1000}, {"CM", 900}, {"D", 500}, {"CD", 400}, {"C", 100}, {"XC", 90}, {"L", 50}, {"XL", 40}, {"X", 10}, {"IX", 9}, {"V", 5}, {"IV", 4}, {"I", 1}, } romanRegex = regexp.MustCompile(`^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$`)}func ToRoman(n int) (string, error) { if n <= 0 || n >= 4000 { return "", ErrOutOfRange } var buf bytes.Buffer for _, pair := range romanNumParis { for n > pair.Num { buf.WriteString(pair.Roman) n -= pair.Num } } return buf.String(), nil}func FromRoman(roman string) (int, error) { if !romanRegex.MatchString(roman) { return 0, ErrInvalidRoman } var result int var index int for _, pair := range romanNumParis { for roman[index:index+len(pair.Roman)] == pair.Roman { result += pair.Num index += len(pair.Roman) } } return result, nil}
在 Go 中编写测试很简略,只须要在待测试性能所在文件的同级目录中创立一个以_test.go
结尾的文件。在该文件中,咱们能够编写一个个测试函数。测试函数名必须是TestXxxx
这个模式,而且Xxxx
必须以大写字母结尾,另外函数带有一个*testing.T
类型的参数:
// roman_test.gopackage romanimport ( "testing")func TestToRoman(t *testing.T) { _, err1 := ToRoman(0) if err1 != ErrOutOfRange { t.Errorf("ToRoman(0) expect error:%v got:%v", ErrOutOfRange, err1) } roman2, err2 := ToRoman(1) if err2 != nil { t.Errorf("ToRoman(1) expect nil error, got:%v", err2) } if roman2 != "I" { t.Errorf("ToRoman(1) expect:%s got:%s", "I", roman2) }}
在测试函数中编写的代码与失常的代码没有什么不同,调用相应的函数,返回后果,判断后果与预期是否统一,如果不统一则调用testing.T
的Errorf()
输入错误信息。运行测试时,这些错误信息会被收集起来,运行完结后对立输入。
测试编写实现之后,应用go test
命令运行测试,输入后果:
$ go test--- FAIL: TestToRoman (0.00s) roman_test.go:18: ToRoman(1) expect:I got:FAILexit status 1FAIL github.com/darjun/go-daily-lib/testing 0.172s
我成心将ToRoman()
函数中写错了一行代码,n > pair.Num
中>
应该为>=
,单元测试胜利找出了谬误。批改之后从新运行测试:
$ go testPASSok github.com/darjun/go-daily-lib/testing 0.178s
这次测试都通过了!
咱们还能够给go test
命令传入-v
选项,输入具体的测试信息:
$ go test -v=== RUN TestToRoman--- PASS: TestToRoman (0.00s)PASSok github.com/darjun/go-daily-lib/testing 0.174s
在运行每个测试函数前,都输入一行=== RUN
,运行完结之后输入--- PASS
或--- FAIL
信息。
表格驱动测试
在下面的例子中,咱们实际上只测试了两种状况,0 和 1。依照这种形式将每种状况都写进去就太繁琐了,Go 中风行应用表格的形式将各个测试数据和后果列举进去:
func TestToRoman(t *testing.T) { testCases := []struct { num int expect string err error }{ {0, "", ErrOutOfRange}, {1, "I", nil}, {2, "II", nil}, {3, "III", nil}, {4, "IV", nil}, {5, "V", nil}, {6, "VI", nil}, {7, "VII", nil}, {8, "VIII", nil}, {9, "IX", nil}, {10, "X", nil}, {50, "L", nil}, {100, "C", nil}, {500, "D", nil}, {1000, "M", nil}, {31, "XXXI", nil}, {148, "CXLVIII", nil}, {294, "CCXCIV", nil}, {312, "CCCXII", nil}, {421, "CDXXI", nil}, {528, "DXXVIII", nil}, {621, "DCXXI", nil}, {782, "DCCLXXXII", nil}, {870, "DCCCLXX", nil}, {941, "CMXLI", nil}, {1043, "MXLIII", nil}, {1110, "MCX", nil}, {1226, "MCCXXVI", nil}, {1301, "MCCCI", nil}, {1485, "MCDLXXXV", nil}, {1509, "MDIX", nil}, {1607, "MDCVII", nil}, {1754, "MDCCLIV", nil}, {1832, "MDCCCXXXII", nil}, {1993, "MCMXCIII", nil}, {2074, "MMLXXIV", nil}, {2152, "MMCLII", nil}, {2212, "MMCCXII", nil}, {2343, "MMCCCXLIII", nil}, {2499, "MMCDXCIX", nil}, {2574, "MMDLXXIV", nil}, {2646, "MMDCXLVI", nil}, {2723, "MMDCCXXIII", nil}, {2892, "MMDCCCXCII", nil}, {2975, "MMCMLXXV", nil}, {3051, "MMMLI", nil}, {3185, "MMMCLXXXV", nil}, {3250, "MMMCCL", nil}, {3313, "MMMCCCXIII", nil}, {3408, "MMMCDVIII", nil}, {3501, "MMMDI", nil}, {3610, "MMMDCX", nil}, {3743, "MMMDCCXLIII", nil}, {3844, "MMMDCCCXLIV", nil}, {3888, "MMMDCCCLXXXVIII", nil}, {3940, "MMMCMXL", nil}, {3999, "MMMCMXCIX", nil}, {4000, "", ErrOutOfRange}, } for _, testCase := range testCases { got, err := ToRoman(testCase.num) if got != testCase.expect { t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got) } if err != testCase.err { t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err) } }}
下面将要测试的每种状况列举进去,而后针对每个整数调用ToRoman()
函数,比拟返回的罗马数字字符串和谬误值是否与预期的相符。后续要增加新的测试用例也很不便。
分组和并行
有时候对同一个函数有不同维度的测试,将这些组合在一起有利于保护。例如上面对ToRoman()
函数的测试能够分为非法值,单个罗马字符和一般 3 种状况。
为了分组,我对代码做了肯定水平的重构,首先形象一个toRomanCase
构造:
type toRomanCase struct { num int expect string err error}
将所有的测试数据划分到 3 个组中:
var ( toRomanInvalidCases []toRomanCase toRomanSingleCases []toRomanCase toRomanNormalCases []toRomanCase)func init() { toRomanInvalidCases = []toRomanCase{ {0, "", roman.ErrOutOfRange}, {4000, "", roman.ErrOutOfRange}, } toRomanSingleCases = []toRomanCase{ {1, "I", nil}, {5, "V", nil}, // ... } toRomanNormalCases = []toRomanCase{ {2, "II", nil}, {3, "III", nil}, // ... }}
而后为了防止代码反复,形象一个运行多个toRomanCase
的函数:
func testToRomanCases(cases []toRomanCase, t *testing.T) { for _, testCase := range cases { got, err := roman.ToRoman(testCase.num) if got != testCase.expect { t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got) } if err != testCase.err { t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err) } }}
为每个分组定义一个测试函数:
func testToRomanInvalid(t *testing.T) { testToRomanCases(toRomanInvalidCases, t)}func testToRomanSingle(t *testing.T) { testToRomanCases(toRomanSingleCases, t)}func testToRomanNormal(t *testing.T) { testToRomanCases(toRomanNormalCases, t)}
在原来的测试函数中,调用t.Run()
运行不同分组的测试函数,t.Run()
第一个参数为子测试名,第二个参数为子测试函数:
func TestToRoman(t *testing.T) { t.Run("Invalid", testToRomanInvalid) t.Run("Single", testToRomanSingle) t.Run("Normal", testToRomanNormal)}
运行:
$ go test -v=== RUN TestToRoman=== RUN TestToRoman/Invalid=== RUN TestToRoman/Single=== RUN TestToRoman/Normal--- PASS: TestToRoman (0.00s) --- PASS: TestToRoman/Invalid (0.00s) --- PASS: TestToRoman/Single (0.00s) --- PASS: TestToRoman/Normal (0.00s)PASSok github.com/darjun/go-daily-lib/testing 0.188s
能够看到,顺次运行 3 个子测试,子测试名是父测试名和t.Run()
指定的名字组合而成的,如TestToRoman/Invalid
。
默认状况下,这些测试都是顺次程序执行的。如果各个测试之间没有分割,咱们能够让他们并行以放慢测试速度。办法也很简略,在testToRomanInvalid/testToRomanSingle/testToRomanNormal
这 3 个函数开始处调用t.Parallel()
,因为这 3 个函数间接调用了testToRomanCases
,也能够只在testToRomanCases
函数结尾出增加:
func testToRomanCases(cases []toRomanCase, t *testing.T) { t.Parallel() // ...}
运行:
$ go test -v...--- PASS: TestToRoman (0.00s) --- PASS: TestToRoman/Invalid (0.00s) --- PASS: TestToRoman/Normal (0.00s) --- PASS: TestToRoman/Single (0.00s)PASSok github.com/darjun/go-daily-lib/testing 0.182s
咱们发现测试实现的程序并不是咱们指定的程序。
另外,这个示例中我将roman_test.go
文件移到了roman_test
包中,所以须要import "github.com/darjun/go-daily-lib/testing/roman"
。这种形式在测试包有循环依赖的状况下十分有用,例如规范库中net/http
依赖net/url
,url
的测试函数依赖net/http
,如果把测试放在net/url
包中,那么就会导致循环依赖url_test(net/url)
->net/http
->net/url
。这时能够将url_test
放在一个独立的包中。
主测试函数
有一种非凡的测试函数,函数名为TestMain()
,承受一个*testing.M
类型的参数。这个函数个别用于在运行所有测试前执行一些初始化逻辑(如创立数据库链接),或所有测试都运行完结之后执行一些清理逻辑(开释数据库链接)。如果测试文件中定义了这个函数,则go test
命令会间接运行这个函数,否者go test
会创立一个默认的TestMain()
函数。这个函数的默认行为就是运行文件中定义的测试。咱们自定义TestMain()
函数时,也须要手动调用m.Run()
办法运行测试函数,否则测试函数不会运行。默认的TestMain()
相似上面代码:
func TestMain(m *testing.M) { os.Exit(m.Run())}
上面自定义一个TestMain()
函数,打印go test
反对的选项:
func TestMain(m *testing.M) { flag.Parse() flag.VisitAll(func(f *flag.Flag) { fmt.Printf("name:%s usage:%s value:%v\n", f.Name, f.Usage, f.Value) }) os.Exit(m.Run())}
运行:
$ go test -vname:test.bench usage:run only benchmarks matching `regexp` value:name:test.benchmem usage:print memory allocations for benchmarks value:falsename:test.benchtime usage:run each benchmark for duration `d` value:1sname:test.blockprofile usage:write a goroutine blocking profile to `file` value:name:test.blockprofilerate usage:set blocking profile `rate` (see runtime.SetBlockProfileRate) value:1name:test.count usage:run tests and benchmarks `n` times value:1name:test.coverprofile usage:write a coverage profile to `file` value:name:test.cpu usage:comma-separated `list` of cpu counts to run each test with value:name:test.cpuprofile usage:write a cpu profile to `file` value:name:test.failfast usage:do not start new tests after the first test failure value:falsename:test.list usage:list tests, examples, and benchmarks matching `regexp` then exit value:name:test.memprofile usage:write an allocation profile to `file` value:name:test.memprofilerate usage:set memory allocation profiling `rate` (see runtime.MemProfileRate) value:0name:test.mutexprofile usage:write a mutex contention profile to the named file after execution value:name:test.mutexprofilefraction usage:if >= 0, calls runtime.SetMutexProfileFraction() value:1name:test.outputdir usage:write profiles to `dir` value:name:test.paniconexit0 usage:panic on call to os.Exit(0) value:truename:test.parallel usage:run at most `n` tests in parallel value:8name:test.run usage:run only tests and examples matching `regexp` value:name:test.short usage:run smaller test suite to save time value:falsename:test.testlogfile usage:write test action log to `file` (for use only by cmd/go) value:name:test.timeout usage:panic test binary after duration `d` (default 0, timeout disabled) value:10m0sname:test.trace usage:write an execution trace to `file` value:name:test.v usage:verbose: print additional output value:tru
这些选项也能够通过go help testflag
查看。
其余
另一个函数FromRoman()
我没有写任何测试,就交给大家了
性能测试
性能测试是为了对函数的运行性能进行评测。性能测试也必须在_test.go
文件中编写,且函数名必须是BenchmarkXxxx
结尾。性能测试函数承受一个*testing.B
的参数。上面咱们编写 3 个计算第 n 个斐波那契数的函数。
第一种形式:递归
func Fib1(n int) int { if n <= 1 { return n } return Fib1(n-1) + Fib1(n-2)}
第二种形式:备忘录
func fibHelper(n int, m map[int]int) int { if n <= 1 { return n } if v, ok := m[n]; ok { return v } v := fibHelper(n-2, m) + fibHelper(n-1, m) m[n] = v return v}func Fib2(n int) int { m := make(map[int]int) return fibHelper(n, m)}
第三种形式:迭代
func Fib3(n int) int { if n <= 1 { return n } f1, f2 := 0, 1 for i := 2; i <= n; i++ { f1, f2 = f2, f1+f2 } return f2}
上面咱们来测试这 3 个函数的执行效率:
// fib_test.gofunc BenchmarkFib1(b *testing.B) { for i := 0; i < b.N; i++ { Fib1(20) }}func BenchmarkFib2(b *testing.B) { for i := 0; i < b.N; i++ { Fib2(20) }}func BenchmarkFib3(b *testing.B) { for i := 0; i < b.N; i++ { Fib3(20) }}
须要特地留神的是N
,go test
会始终调整这个数值,直到测试工夫能得出牢靠的性能数据为止。运行:
$ go test -bench=.goos: windowsgoarch: amd64pkg: github.com/darjun/go-daily-lib/testing/fibcpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHzBenchmarkFib1-8 31110 39144 ns/opBenchmarkFib2-8 582637 3127 ns/opBenchmarkFib3-8 191600582 5.588 ns/opPASSok github.com/darjun/go-daily-lib/testing/fib 5.225s
性能测试默认不会执行,须要通过-bench=.
指定运行。-bench
选项的值是一个简略的模式,.
示意匹配所有的,Fib
示意运行名字中有Fib
的。
下面的测试后果示意Fib1
在指定工夫内执行了 31110 次,均匀每次 39144 ns,Fib2
在指定工夫内运行了 582637 次,均匀每次耗时 3127 ns,Fib3
在指定工夫内运行了 191600582 次,均匀每次耗时 5.588 ns。
其余选项
有一些选项能够管制性能测试的执行。
-benchtime
:设置每个测试的运行工夫。
$ go test -bench=. -benchtime=30s
运行了更长的工夫:
$ go test -bench=. -benchtime=30sgoos: windowsgoarch: amd64pkg: github.com/darjun/go-daily-lib/testing/fibcpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHzBenchmarkFib1-8 956464 38756 ns/opBenchmarkFib2-8 17862495 2306 ns/opBenchmarkFib3-8 1000000000 5.591 ns/opPASSok github.com/darjun/go-daily-lib/testing/fib 113.498s
-benchmem
:输入性能测试函数的内存分配情况。
-memprofile file
:将内存调配数据写入文件。
-cpuprofile file
:将 CPU 采样数据写入文件,方便使用go tool pprof
工具剖析,详见我的另一篇文章《你不晓得的 Go 之 pprof》
运行:
$ go test -bench=. -benchtime=10s -cpuprofile=./cpu.prof -memprofile=./mem.profgoos: windowsgoarch: amd64pkg: github.com/darjun/fibBenchmarkFib1-16 356006 33423 ns/opBenchmarkFib2-16 8958194 1340 ns/opBenchmarkFib3-16 1000000000 6.60 ns/opPASSok github.com/darjun/fib 33.321s
同时生成了 CPU 采样数据和内存调配数据,通过go tool pprof
剖析:
$ go tool pprof ./cpu.profType: cpuTime: Aug 4, 2021 at 10:21am (CST)Duration: 32.48s, Total samples = 36.64s (112.81%)Entering interactive mode (type "help" for commands, "o" for options)(pprof) top10Showing nodes accounting for 29640ms, 80.90% of 36640ms totalDropped 153 nodes (cum <= 183.20ms)Showing top 10 nodes out of 74 flat flat% sum% cum cum% 11610ms 31.69% 31.69% 11620ms 31.71% github.com/darjun/fib.Fib1 6490ms 17.71% 49.40% 6680ms 18.23% github.com/darjun/fib.Fib3 2550ms 6.96% 56.36% 8740ms 23.85% runtime.mapassign_fast64 2050ms 5.59% 61.95% 2060ms 5.62% runtime.stdcall2 1620ms 4.42% 66.38% 2140ms 5.84% runtime.mapaccess2_fast64 1480ms 4.04% 70.41% 12350ms 33.71% github.com/darjun/fib.fibHelper 1480ms 4.04% 74.45% 2960ms 8.08% runtime.evacuate_fast64 1050ms 2.87% 77.32% 1050ms 2.87% runtime.memhash64 760ms 2.07% 79.39% 760ms 2.07% runtime.stdcall7 550ms 1.50% 80.90% 7230ms 19.73% github.com/darjun/fib.BenchmarkFib3(pprof)
内存:
$ go tool pprof ./mem.profType: alloc_spaceTime: Aug 4, 2021 at 10:30am (CST)Entering interactive mode (type "help" for commands, "o" for options)(pprof) top10Showing nodes accounting for 8.69GB, 100% of 8.69GB totalDropped 12 nodes (cum <= 0.04GB) flat flat% sum% cum cum% 8.69GB 100% 100% 8.69GB 100% github.com/darjun/fib.fibHelper 0 0% 100% 8.69GB 100% github.com/darjun/fib.BenchmarkFib2 0 0% 100% 8.69GB 100% github.com/darjun/fib.Fib2 (inline) 0 0% 100% 8.69GB 100% testing.(*B).launch 0 0% 100% 8.69GB 100% testing.(*B).runN(pprof)
示例测试
示例测试用于演示模块或函数的应用。同样地,示例测试也在文件_test.go
中编写,并且示例测试函数名必须是ExampleXxx
的模式。在Example*
函数中编写代码,而后在正文中编写冀望的输入,go test
会运行该函数,而后将理论输入与冀望的做比拟。上面摘取自 Go 源码net/url/example_test.go
文件中的代码演示了url.Values
的用法:
func ExampleValuesGet() { v := url.Values{} v.Set("name", "Ava") v.Add("friend", "Jess") v.Add("friend", "Sarah") v.Add("friend", "Zoe") fmt.Println(v.Get("name")) fmt.Println(v.Get("friend")) fmt.Println(v["friend"]) // Output: // Ava // Jess // [Jess Sarah Zoe]}
正文中Output:
后是冀望的输入后果,go test
会运行这些函数并与冀望的后果做比拟,比拟会疏忽空格。
有时候咱们输入的程序是不确定的,这时就须要应用Unordered Output
。咱们晓得url.Values
底层类型为map[string][]string
,所以能够遍历输入所有的键值,然而输入程序不确定:
func ExampleValuesAll() { v := url.Values{} v.Set("name", "Ava") v.Add("friend", "Jess") v.Add("friend", "Sarah") v.Add("friend", "Zoe") for key, values := range v { fmt.Println(key, values) } // Unordered Output: // name [Ava] // friend [Jess Sarah Zoe]}
运行:
$ go test -v$ go test -v=== RUN ExampleValuesGet--- PASS: ExampleValuesGet (0.00s)=== RUN ExampleValuesAll--- PASS: ExampleValuesAll (0.00s)PASSok github.com/darjun/url 0.172s
没有正文,或正文中无Output/Unordered Output
的函数会被疏忽。
总结
本文介绍了 Go 中的 3 种测试:单元测试,性能测试和示例测试。为了让程序更牢靠,让当前的重构更平安、更释怀,单元测试必不可少。排查程序中的性能问题,性能测试能派上大用场。示例测试次要是为了演示如何应用某个性能。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue
参考
- testing 官网文档: https://golang.google.cn/pkg/testing/
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~