Go語言自帶測試庫testing使用教程
簡介
testing是 Go 語言標準庫自帶的測試庫。在 Go 語言中編寫測試很簡單,只需要遵循 Go 測試的幾個約定,與編寫正常的 Go 代碼沒有什么區(qū)別。Go 語言中有 3 種類型的測試:單元測試,性能測試,示例測試。下面依次來介紹。
單元測試
單元測試又稱為功能性測試,是為了測試函數(shù)、模塊等代碼的邏輯是否正確。接下來我們編寫一個庫,用于將表示羅馬數(shù)字的字符串和整數(shù)互轉。羅馬數(shù)字是由M/D/C/L/X/V/I這幾個字符根據(jù)一定的規(guī)則組合起來表示一個正整數(shù):
- M=1000,D=500,C=100,L=50,X=10,V=5,I=1;
- 只能表示 1-3999 范圍內的整數(shù),不能表示 0 和負數(shù),不能表示 4000 及以上的整數(shù),不能表示分數(shù)和小數(shù)(當然有其他復雜的規(guī)則來表示這些數(shù)字,這里暫不考慮);
- 每個整數(shù)只有一種表示方式,一般情況下,連寫的字符表示對應整數(shù)相加,例如
I=1,II=2,III=3。但是,十位字符(I/X/C/M)最多出現(xiàn) 3 次,所以不能用IIII表示 4,需要在V左邊添加一個I(即IV)來表示,不能用VIIII表示 9,需要使用IX代替。另外五位字符(V/L/D)不能連續(xù)出現(xiàn) 2 次,所以不能出現(xiàn)VV,需要用X代替。
// roman.go
package roman
import (
"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 中編寫測試很簡單,只需要在待測試功能所在文件的同級目錄中創(chuàng)建一個以_test.go結尾的文件。在該文件中,我們可以編寫一個個測試函數(shù)。測試函數(shù)名必須是TestXxxx這個形式,而且Xxxx必須以大寫字母開頭,另外函數(shù)帶有一個*testing.T類型的參數(shù):
// roman_test.go
package roman
import (
"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)
}
}在測試函數(shù)中編寫的代碼與正常的代碼沒有什么不同,調用相應的函數(shù),返回結果,判斷結果與預期是否一致,如果不一致則調用testing.T的Errorf()輸出錯誤信息。運行測試時,這些錯誤信息會被收集起來,運行結束后統(tǒng)一輸出。
測試編寫完成之后,使用go test命令運行測試,輸出結果:
$ go test
--- FAIL: TestToRoman (0.00s)
roman_test.go:18: ToRoman(1) expect:I got:
FAIL
exit status 1
FAIL github.com/darjun/go-daily-lib/testing 0.172s
我故意將ToRoman()函數(shù)中寫錯了一行代碼,n > pair.Num中>應該為>=,單元測試成功找出了錯誤。修改之后重新運行測試:
$ go test PASS ok github.com/darjun/go-daily-lib/testing 0.178s
這次測試都通過了!
我們還可以給go test命令傳入-v選項,輸出詳細的測試信息:
$ go test -v
=== RUN TestToRoman
--- PASS: TestToRoman (0.00s)
PASS
ok github.com/darjun/go-daily-lib/testing 0.174s
在運行每個測試函數(shù)前,都輸出一行=== RUN,運行結束之后輸出--- PASS或--- FAIL信息。
表格驅動測試
在上面的例子中,我們實際上只測試了兩種情況,0 和 1。按照這種方式將每種情況都寫出來就太繁瑣了,Go 中流行使用表格的方式將各個測試數(shù)據(jù)和結果列舉出來:
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)
}
}
}上面將要測試的每種情況列舉出來,然后針對每個整數(shù)調用ToRoman()函數(shù),比較返回的羅馬數(shù)字字符串和錯誤值是否與預期的相符。后續(xù)要添加新的測試用例也很方便。
分組和并行
有時候對同一個函數(shù)有不同維度的測試,將這些組合在一起有利于維護。例如上面對ToRoman()函數(shù)的測試可以分為非法值,單個羅馬字符和普通 3 種情況。
為了分組,我對代碼做了一定程度的重構,首先抽象一個toRomanCase結構:
type toRomanCase struct {
num int
expect string
err error
}將所有的測試數(shù)據(jù)劃分到 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的函數(shù):
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)
}
}
}為每個分組定義一個測試函數(shù):
func testToRomanInvalid(t *testing.T) {
testToRomanCases(toRomanInvalidCases, t)
}
func testToRomanSingle(t *testing.T) {
testToRomanCases(toRomanSingleCases, t)
}
func testToRomanNormal(t *testing.T) {
testToRomanCases(toRomanNormalCases, t)
}在原來的測試函數(shù)中,調用t.Run()運行不同分組的測試函數(shù),t.Run()第一個參數(shù)為子測試名,第二個參數(shù)為子測試函數(shù):
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)
PASS
ok github.com/darjun/go-daily-lib/testing 0.188s
可以看到,依次運行 3 個子測試,子測試名是父測試名和t.Run()指定的名字組合而成的,如TestToRoman/Invalid。
默認情況下,這些測試都是依次順序執(zhí)行的。如果各個測試之間沒有聯(lián)系,我們可以讓他們并行以加快測試速度。方法也很簡單,在testToRomanInvalid/testToRomanSingle/testToRomanNormal這 3 個函數(shù)開始處調用t.Parallel(),由于這 3 個函數(shù)直接調用了testToRomanCases,也可以只在testToRomanCases函數(shù)開頭出添加:
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)
PASS
ok github.com/darjun/go-daily-lib/testing 0.182s我們發(fā)現(xiàn)測試完成的順序并不是我們指定的順序。
另外,這個示例中我將roman_test.go文件移到了roman_test包中,所以需要import "github.com/darjun/go-daily-lib/testing/roman"。這種方式在測試包有循環(huán)依賴的情況下非常有用,例如標準庫中net/http依賴net/url,url的測試函數(shù)依賴net/http,如果把測試放在net/url包中,那么就會導致循環(huán)依賴url_test(net/url)->net/http->net/url。這時可以將url_test放在一個獨立的包中。
主測試函數(shù)
有一種特殊的測試函數(shù),函數(shù)名為TestMain(),接受一個*testing.M類型的參數(shù)。這個函數(shù)一般用于在運行所有測試前執(zhí)行一些初始化邏輯(如創(chuàng)建數(shù)據(jù)庫鏈接),或所有測試都運行結束之后執(zhí)行一些清理邏輯(釋放數(shù)據(jù)庫鏈接)。如果測試文件中定義了這個函數(shù),則go test命令會直接運行這個函數(shù),否者go test會創(chuàng)建一個默認的TestMain()函數(shù)。這個函數(shù)的默認行為就是運行文件中定義的測試。我們自定義TestMain()函數(shù)時,也需要手動調用m.Run()方法運行測試函數(shù),否則測試函數(shù)不會運行。默認的TestMain()類似下面代碼:
func TestMain(m *testing.M) {
os.Exit(m.Run())
}下面自定義一個TestMain()函數(shù),打印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 -v name:test.bench usage:run only benchmarks matching `regexp` value: name:test.benchmem usage:print memory allocations for benchmarks value:false name:test.benchtime usage:run each benchmark for duration `d` value:1s name:test.blockprofile usage:write a goroutine blocking profile to `file` value: name:test.blockprofilerate usage:set blocking profile `rate` (see runtime.SetBlockProfileRate) value:1 name:test.count usage:run tests and benchmarks `n` times value:1 name: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:false name: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:0 name: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:1 name:test.outputdir usage:write profiles to `dir` value: name:test.paniconexit0 usage:panic on call to os.Exit(0) value:true name:test.parallel usage:run at most `n` tests in parallel value:8 name:test.run usage:run only tests and examples matching `regexp` value: name:test.short usage:run smaller test suite to save time value:false name: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:10m0s name:test.trace usage:write an execution trace to `file` value: name:test.v usage:verbose: print additional output value:tru
這些選項也可以通過go help testflag查看。
其他
另一個函數(shù)FromRoman()我沒有寫任何測試,就交給大家了??
性能測試
性能測試是為了對函數(shù)的運行性能進行評測。性能測試也必須在_test.go文件中編寫,且函數(shù)名必須是BenchmarkXxxx開頭。性能測試函數(shù)接受一個*testing.B的參數(shù)。下面我們編寫 3 個計算第 n 個斐波那契數(shù)的函數(shù)。
第一種方式:遞歸
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 個函數(shù)的執(zhí)行效率:
// fib_test.go
func 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會一直調整這個數(shù)值,直到測試時間能得出可靠的性能數(shù)據(jù)為止。運行:
$ go test -bench=. goos: windows goarch: amd64 pkg: github.com/darjun/go-daily-lib/testing/fib cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz BenchmarkFib1-8 31110 39144 ns/op BenchmarkFib2-8 582637 3127 ns/op BenchmarkFib3-8 191600582 5.588 ns/op PASS ok github.com/darjun/go-daily-lib/testing/fib 5.225s
性能測試默認不會執(zhí)行,需要通過-bench=.指定運行。-bench選項的值是一個簡單的模式,.表示匹配所有的,Fib表示運行名字中有Fib的。
上面的測試結果表示Fib1在指定時間內執(zhí)行了 31110 次,平均每次 39144 ns,Fib2在指定時間內運行了 582637 次,平均每次耗時 3127 ns,Fib3在指定時間內運行了 191600582 次,平均每次耗時 5.588 ns。
其他選項
有一些選項可以控制性能測試的執(zhí)行。
-benchtime:設置每個測試的運行時間。
$ go test -bench=. -benchtime=30s
運行了更長的時間:
$ go test -bench=. -benchtime=30s goos: windows goarch: amd64 pkg: github.com/darjun/go-daily-lib/testing/fib cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz BenchmarkFib1-8 956464 38756 ns/op BenchmarkFib2-8 17862495 2306 ns/op BenchmarkFib3-8 1000000000 5.591 ns/op PASS ok github.com/darjun/go-daily-lib/testing/fib 113.498s
-benchmem:輸出性能測試函數(shù)的內存分配情況。
-memprofile file:將內存分配數(shù)據(jù)寫入文件。
-cpuprofile file:將 CPU 采樣數(shù)據(jù)寫入文件,方便使用go tool pprof工具分析,詳見我的另一篇文章《你不知道的 Go 之 pprof》
運行:
$ go test -bench=. -benchtime=10s -cpuprofile=./cpu.prof -memprofile=./mem.prof goos: windows goarch: amd64 pkg: github.com/darjun/fib BenchmarkFib1-16 356006 33423 ns/op BenchmarkFib2-16 8958194 1340 ns/op BenchmarkFib3-16 1000000000 6.60 ns/op PASS ok github.com/darjun/fib 33.321s
同時生成了 CPU 采樣數(shù)據(jù)和內存分配數(shù)據(jù),通過go tool pprof分析:
$ go tool pprof ./cpu.prof
Type: cpu
Time: 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) top10
Showing nodes accounting for 29640ms, 80.90% of 36640ms total
Dropped 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.prof
Type: alloc_space
Time: Aug 4, 2021 at 10:30am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 8.69GB, 100% of 8.69GB total
Dropped 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)示例測試
示例測試用于演示模塊或函數(shù)的使用。同樣地,示例測試也在文件_test.go中編寫,并且示例測試函數(shù)名必須是ExampleXxx的形式。在Example*函數(shù)中編寫代碼,然后在注釋中編寫期望的輸出,go test會運行該函數(shù),然后將實際輸出與期望的做比較。下面摘取自 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會運行這些函數(shù)并與期望的結果做比較,比較會忽略空格。
有時候我們輸出的順序是不確定的,這時就需要使用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)
PASS
ok github.com/darjun/url 0.172s
沒有注釋,或注釋中無Output/Unordered Output的函數(shù)會被忽略。
總結
本文介紹了 Go 中的 3 種測試:單元測試,性能測試和示例測試。為了讓程序更可靠,讓以后的重構更安全、更放心,單元測試必不可少。排查程序中的性能問題,性能測試能派上大用場。示例測試主要是為了演示如何使用某個功能。
參考
- testing 官方文檔: https://golang.google.cn/pkg/testing/
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
以上就是Go語言自帶測試庫testing使用教程的詳細內容,更多關于Go語言測試庫testing的資料請關注腳本之家其它相關文章!
相關文章
VSCode Golang dlv調試數(shù)據(jù)截斷問題及處理方法
這篇文章主要介紹了VSCode Golang dlv調試數(shù)據(jù)截斷問題,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
GoLang jwt無感刷新與SSO單點登錄限制解除方法詳解
這篇文章主要介紹了GoLang jwt無感刷新與SSO單點登錄限制解除方法,JWT是一個簽名的JSON對象,通常用作Oauth2的Bearer token,JWT包括三個用.分割的部分。本文將利用JWT進行認證和加密,感興趣的可以了解一下2023-03-03

