今天就跟大家聊聊有關(guān)如何編寫(xiě)可測(cè)試的Go語(yǔ)言代碼,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
創(chuàng)新互聯(lián)建站專注于中大型企業(yè)的做網(wǎng)站、成都網(wǎng)站建設(shè)和網(wǎng)站改版、網(wǎng)站營(yíng)銷(xiāo)服務(wù),追求商業(yè)策劃與數(shù)據(jù)分析、創(chuàng)意藝術(shù)與技術(shù)開(kāi)發(fā)的融合,累計(jì)客戶超過(guò)千家,服務(wù)滿意度達(dá)97%。幫助廣大客戶順利對(duì)接上互聯(lián)網(wǎng)浪潮,準(zhǔn)確優(yōu)選出符合自己需要的互聯(lián)網(wǎng)運(yùn)用,我們將一直專注品牌網(wǎng)站建設(shè)和互聯(lián)網(wǎng)程序開(kāi)發(fā),在前進(jìn)的路上,與客戶一起成長(zhǎng)!
第一個(gè)測(cè)試 “Hello Test!”
首先,在我們$GOPATH/src目錄下創(chuàng)建hello目錄,作為本文涉及到的所有示例代碼的根目錄。
然后,新建名為hello.go的文件,定義一個(gè)函數(shù)hello()
,功能是返回一個(gè)由若干單詞拼接成句子:
package hello func hello() string { words := []string{"hello", "func", "in", "package", "hello"} wl := len(words) sentence := "" for key, word := range words { sentence += word if key < wl-1 { sentence += " " } else { sentence += "." } } return sentence }
接著,新建名為hello_test.go的文件,填入如下內(nèi)容:
package hello import ( "fmt" "testing" ) func TestHello(t *testing.T) { got := hello() expect := "hello func in package hello." if got != expect { t.Errorf("got [%s] expected [%s]", got, expect) } } func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { hello() } } func ExampleHello() { hl := hello() fmt.Println(hl) // Output: hello func in package hello. }
最后,打開(kāi)終端,進(jìn)入hello目錄,輸入go test
命令并回車(chē),可以看到如下輸出:
PASS ok hello 0.007s
編寫(xiě)測(cè)試代碼
Golang的測(cè)試代碼位于某個(gè)包的源代碼中名稱以_test.go結(jié)尾的源文件里,測(cè)試代碼包含測(cè)試函數(shù)、測(cè)試輔助代碼和示例函數(shù);測(cè)試函數(shù)有以Test開(kāi)頭的功能測(cè)試函數(shù)和以Benchmark開(kāi)頭的性能測(cè)試函數(shù)兩種,測(cè)試輔助代碼是為測(cè)試函數(shù)服務(wù)的公共函數(shù)、初始化函數(shù)、測(cè)試數(shù)據(jù)等,示例函數(shù)則是以Example開(kāi)頭的說(shuō)明被測(cè)試函數(shù)用法的函數(shù)。
大部分情況下,測(cè)試代碼是作為某個(gè)包的一部分,意味著它可以訪問(wèn)包中不可導(dǎo)出的元素。但在有需要的時(shí)候(如避免循環(huán)依賴)也可以修改測(cè)試文件的包名,如package hello的測(cè)試文件,包名可以設(shè)為package hello_test。
功能測(cè)試函數(shù)
功能測(cè)試函數(shù)需要接收*testing.T類(lèi)型的單一參數(shù)t,testing.T 類(lèi)型用來(lái)管理測(cè)試狀態(tài)和支持格式化的測(cè)試日志。測(cè)試日志在測(cè)試執(zhí)行過(guò)程中積累起來(lái),完成后輸出到標(biāo)準(zhǔn)錯(cuò)誤輸出。
下面是從Go標(biāo)準(zhǔn)庫(kù)摘抄的 testing.T類(lèi)型的常用方法的用法:
測(cè)試函數(shù)中的某條測(cè)試用例執(zhí)行結(jié)果與預(yù)期不符時(shí),調(diào)用t.Error()
或t.Errorf()
方法記錄日志并標(biāo)記測(cè)試失敗
# /usr/local/go/src/bytes/compare_test.go func TestCompareIdenticalSlice(t *testing.T) { var b = []byte("Hello Gophers!") if Compare(b, b) != 0 { t.Error("b != b") } if Compare(b, b[:1]) != 1 { t.Error("b > b[:1] failed") } }
使用t.Fatal()
和t.Fatalf()
方法,在某條測(cè)試用例失敗后就跳出該測(cè)試函數(shù)
# /usr/local/go/src/bytes/reader_test.go func TestReadAfterBigSeek(t *testing.T) { r := NewReader([]byte("0123456789")) if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil { t.Fatal(err) } if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF { t.Errorf("Read = %d, %v; want 0, EOF", n, err) } }
使用t.Skip()
和t.Skipf()
方法,跳過(guò)某條測(cè)試用例的執(zhí)行
# /usr/local/go/src/archive/zip/zip_test.go func TestZip64(t *testing.T) { if testing.Short() { t.Skip("slow test; skipping") } const size = 1 << 32 // before the "END\n" part buf := testZip64(t, size) testZip64DirectoryRecordLength(buf, t) }
執(zhí)行測(cè)試用例的過(guò)程中通過(guò)t.Log()
和t.Logf()
記錄日志
# /usr/local/go/src/regexp/exec_test.go func TestFowler(t *testing.T) { files, err := filepath.Glob("testdata/*.dat") if err != nil { t.Fatal(err) } for _, file := range files { t.Log(file) testFowler(t, file) } }
使用t.Parallel()
標(biāo)記需要并發(fā)執(zhí)行的測(cè)試函數(shù)
# /usr/local/go/src/runtime/stack_test.go func TestStackGrowth(t *testing.T) { t.Parallel() var wg sync.WaitGroup // in a normal goroutine wg.Add(1) go func() { defer wg.Done() growStack() }() wg.Wait() // ... }
性能測(cè)試函數(shù)
性能測(cè)試函數(shù)需要接收*testing.B類(lèi)型的單一參數(shù)b,性能測(cè)試函數(shù)中需要循環(huán)b.N次調(diào)用被測(cè)函數(shù)。testing.B 類(lèi)型用來(lái)管理測(cè)試時(shí)間和迭代運(yùn)行次數(shù),也支持和testing.T相同的方式管理測(cè)試狀態(tài)和格式化的測(cè)試日志,不一樣的是testing.B的日志總是會(huì)輸出。
下面是從Go標(biāo)準(zhǔn)庫(kù)摘抄的 testing.B類(lèi)型的常用方法的用法:
在函數(shù)中調(diào)用t.ReportAllocs()
,啟用內(nèi)存使用分析
# /usr/local/go/src/bufio/bufio_test.go func BenchmarkWriterFlush(b *testing.B) { b.ReportAllocs() bw := NewWriter(ioutil.Discard) str := strings.Repeat("x", 50) for i := 0; i < b.N; i++ { bw.WriteString(str) bw.Flush() } }
通過(guò) b.StopTimer()
、b.ResetTimer()
、b.StartTimer()
來(lái)停止、重置、啟動(dòng) 時(shí)間經(jīng)過(guò)和內(nèi)存分配計(jì)數(shù)
# /usr/local/go/src/fmt/scan_test.go func BenchmarkScanInts(b *testing.B) { b.ResetTimer() ints := makeInts(intCount) var r RecursiveInt for i := b.N - 1; i >= 0; i-- { buf := bytes.NewBuffer(ints) b.StartTimer() scanInts(&r, buf) b.StopTimer() } }
調(diào)用b.SetBytes()
記錄在一個(gè)操作中處理的字節(jié)數(shù)
# /usr/local/go/src/testing/benchmark.go func BenchmarkFields(b *testing.B) { b.SetBytes(int64(len(fieldsInput))) for i := 0; i < b.N; i++ { Fields(fieldsInput) } }
通過(guò)b.RunParallel()
方法和 *testing.PB類(lèi)型的Next()
方法來(lái)并發(fā)執(zhí)行被測(cè)對(duì)象
# /usr/local/go/src/sync/atomic/value_test.go func BenchmarkValueRead(b *testing.B) { var v Value v.Store(new(int)) b.RunParallel(func(pb *testing.PB) { for pb.Next() { x := v.Load().(*int) if *x != 0 { b.Fatalf("wrong value: got %v, want 0", *x) } } }) }
測(cè)試輔助代碼
測(cè)試輔助代碼是編寫(xiě)測(cè)試代碼過(guò)程中因代碼重用和代碼質(zhì)量考慮而產(chǎn)生的。主要包括如下方面:
引入依賴的外部包,如每個(gè)測(cè)試文件都需要的 testing 包等:
# /usr/local/go/src/log/log_test.go: import ( "bytes" "fmt" "os" "regexp" "strings" "testing" "time" )
定義多次用到的常量和變量,測(cè)試用例數(shù)據(jù)等:
# /usr/local/go/src/log/log_test.go: const ( Rdate = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]` Rtime = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]` Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]` Rline = `(57|59):` // must update if the calls to l.Printf / l.Print below move Rlongfile = `.*/[A-Za-z0-9_\-]+\.go:` + Rline Rshortfile = `[A-Za-z0-9_\-]+\.go:` + Rline ) // ... var tests = []tester{ // individual pieces: {0, "", ""}, {0, "XXX", "XXX"}, {Ldate, "", Rdate + " "}, {Ltime, "", Rtime + " "}, {Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "}, {Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time {Llongfile, "", Rlongfile + " "}, {Lshortfile, "", Rshortfile + " "}, {Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile // everything at once: {Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "}, {Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "}, }
和普通的Golang源代碼一樣,測(cè)試代碼中也能定義init函數(shù),init函數(shù)會(huì)在引入外部包、定義常量、聲明變量之后被自動(dòng)調(diào)用,可以在init函數(shù)里編寫(xiě)測(cè)試相關(guān)的初始化代碼。
# /usr/local/go/src/bytes/buffer_test.go func init() { testBytes = make([]byte, N) for i := 0; i < N; i++ { testBytes[i] = 'a' + byte(i%26) } data = string(testBytes) }
封裝測(cè)試專用的公共函數(shù),抽象測(cè)試專用的結(jié)構(gòu)體等:
# /usr/local/go/src/log/log_test.go: type tester struct { flag int prefix string pattern string // regexp that log output must match; we add ^ and expected_text$ always } // ... func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) { // ... }
示例函數(shù)
示例函數(shù)無(wú)需接收參數(shù),但需要使用注釋的 Output: 標(biāo)記說(shuō)明示例函數(shù)的輸出值,未指定Output:標(biāo)記或輸出值為空的示例函數(shù)不會(huì)被執(zhí)行。
示例函數(shù)需要?dú)w屬于某個(gè) 包/函數(shù)/類(lèi)型/類(lèi)型 的方法,具體命名規(guī)則如下:
func Example() { ... } # 包的示例函數(shù) func ExampleF() { ... } # 函數(shù)F的示例函數(shù) func ExampleT() { ... } # 類(lèi)型T的示例函數(shù) func ExampleT_M() { ... } # 類(lèi)型T的M方法的示例函數(shù) # 多示例函數(shù) 需要跟下劃線加小寫(xiě)字母開(kāi)頭的后綴 func Example_suffix() { ... } func ExampleF_suffix() { ... } func ExampleT_suffix() { ... } func ExampleT_M_suffix() { ... }
go doc 工具會(huì)解析示例函數(shù)的函數(shù)體作為對(duì)應(yīng) 包/函數(shù)/類(lèi)型/類(lèi)型的方法 的用法。
測(cè)試函數(shù)的相關(guān)說(shuō)明,可以通過(guò)go help testfunc來(lái)查看幫助文檔。
使用 go test 工具
Golang中通過(guò)命令行工具go test來(lái)執(zhí)行測(cè)試代碼,打開(kāi)shell終端,進(jìn)入需要測(cè)試的包所在的目錄執(zhí)行 go test,或者直接執(zhí)行go test $pkg_name_in_gopath
即可對(duì)指定的包執(zhí)行測(cè)試。
通過(guò)形如go test github.com/tabalt/...
的命令可以執(zhí)行$GOPATH/github.com/tabalt/
目錄下所有的項(xiàng)目的測(cè)試。go test std
命令則可以執(zhí)行Golang標(biāo)準(zhǔn)庫(kù)的所有測(cè)試。
如果想查看執(zhí)行了哪些測(cè)試函數(shù)及函數(shù)的執(zhí)行結(jié)果,可以使用-v參數(shù):
[tabalt@localhost hello] go test -v === RUN TestHello --- PASS: TestHello (0.00s) === RUN ExampleHello --- PASS: ExampleHello (0.00s) PASS ok hello 0.006s
假設(shè)我們有很多功能測(cè)試函數(shù),但某次測(cè)試只想執(zhí)行其中的某一些,可以通過(guò)-run參數(shù),使用正則表達(dá)式來(lái)匹配要執(zhí)行的功能測(cè)試函數(shù)名。如下面指定參數(shù)后,功能測(cè)試函數(shù)TestHello不會(huì)執(zhí)行到。
[tabalt@localhost hello] go test -v -run=xxx PASS ok hello 0.006s
性能測(cè)試函數(shù)默認(rèn)并不會(huì)執(zhí)行,需要添加-bench參數(shù),并指定匹配性能測(cè)試函數(shù)名的正則表達(dá)式;例如,想要執(zhí)行某個(gè)包中所有的性能測(cè)試函數(shù)可以添加參數(shù)-bench . 或 -bench=.。
[tabalt@localhost hello] go test -bench=. PASS BenchmarkHello-8 2000000 657 ns/op ok hello 1.993s
想要查看性能測(cè)試時(shí)的內(nèi)存情況,可以再添加參數(shù)-benchmem:
[tabalt@localhost hello] go test -bench=. -benchmem PASS BenchmarkHello-8 2000000 666 ns/op 208 B/op 9 allocs/op ok hello 2.014s
參數(shù)-cover可以用來(lái)查看我們編寫(xiě)的測(cè)試對(duì)代碼的覆蓋率:
詳細(xì)的覆蓋率信息,可以通過(guò)-coverprofile輸出到文件,并使用go tool cover來(lái)查看,用法請(qǐng)參考go tool cover -help
。
更多go test
命令的參數(shù)及用法,可以通過(guò)go help testflag
來(lái)查看幫助文檔。
高級(jí)測(cè)試技術(shù)
IO相關(guān)測(cè)試
testing/iotest包中實(shí)現(xiàn)了常用的出錯(cuò)的Reader和Writer,可供我們?cè)趇o相關(guān)的測(cè)試中使用。主要有:
觸發(fā)數(shù)據(jù)錯(cuò)誤dataErrReader,通過(guò)DataErrReader()
函數(shù)創(chuàng)建
讀取一半內(nèi)容的halfReader,通過(guò)HalfReader()
函數(shù)創(chuàng)建
讀取一個(gè)byte的oneByteReader,通過(guò)OneByteReader()
函數(shù)創(chuàng)建
觸發(fā)超時(shí)錯(cuò)誤的timeoutReader,通過(guò)TimeoutReader()
函數(shù)創(chuàng)建
寫(xiě)入指定位數(shù)內(nèi)容后停止的truncateWriter,通過(guò)TruncateWriter()
函數(shù)創(chuàng)建
讀取時(shí)記錄日志的readLogger,通過(guò)NewReadLogger()
函數(shù)創(chuàng)建
寫(xiě)入時(shí)記錄日志的writeLogger,通過(guò)NewWriteLogger()
函數(shù)創(chuàng)建
黑盒測(cè)試
testing/quick包實(shí)現(xiàn)了幫助黑盒測(cè)試的實(shí)用函數(shù) Check和CheckEqual。
Check函數(shù)的第1個(gè)參數(shù)是要測(cè)試的只返回bool值的黑盒函數(shù)f,Check會(huì)為f的每個(gè)參數(shù)設(shè)置任意值并多次調(diào)用,如果f返回false,Check函數(shù)會(huì)返回錯(cuò)誤值 *CheckError。Check函數(shù)的第2個(gè)參數(shù) 可以指定一個(gè)quick.Config類(lèi)型的config,傳nil則會(huì)默認(rèn)使用quick.defaultConfig。quick.Config結(jié)構(gòu)體包含了測(cè)試運(yùn)行的選項(xiàng)。
# /usr/local/go/src/math/big/int_test.go func checkMul(a, b []byte) bool { var x, y, z1 Int x.SetBytes(a) y.SetBytes(b) z1.Mul(&x, &y) var z2 Int z2.SetBytes(mulBytes(a, b)) return z1.Cmp(&z2) == 0 } func TestMul(t *testing.T) { if err := quick.Check(checkMul, nil); err != nil { t.Error(err) } }
CheckEqual函數(shù)是比較給定的兩個(gè)黑盒函數(shù)是否相等,函數(shù)原型如下:
func CheckEqual(f, g interface{}, config *Config) (err error)
HTTP測(cè)試
net/http/httptest包提供了HTTP相關(guān)代碼的工具,我們的測(cè)試代碼中可以創(chuàng)建一個(gè)臨時(shí)的httptest.Server來(lái)測(cè)試發(fā)送HTTP請(qǐng)求的代碼:
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting)
還可以創(chuàng)建一個(gè)應(yīng)答的記錄器httptest.ResponseRecorder
來(lái)檢測(cè)應(yīng)答的內(nèi)容:
handler := func(w http.ResponseWriter, r *http.Request) { http.Error(w, "something failed", http.StatusInternalServerError) } req, err := http.NewRequest("GET", "http://example.com/foo", nil) if err != nil { log.Fatal(err) } w := httptest.NewRecorder() handler(w, req) fmt.Printf("%d - %s", w.Code, w.Body.String())
測(cè)試進(jìn)程操作行為
當(dāng)我們被測(cè)函數(shù)有操作進(jìn)程的行為,可以將被測(cè)程序作為一個(gè)子進(jìn)程執(zhí)行測(cè)試。下面是一個(gè)例子:
//被測(cè)試的進(jìn)程退出函數(shù) func Crasher() { fmt.Println("Going down in flames!") os.Exit(1) } //測(cè)試進(jìn)程退出函數(shù)的測(cè)試函數(shù) func TestCrasher(t *testing.T) { if os.Getenv("BE_CRASHER") == "1" { Crasher() return } cmd := exec.Command(os.Args[0], "-test.run=TestCrasher") cmd.Env = append(os.Environ(), "BE_CRASHER=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Fatalf("process ran with err %v, want exit status 1", err) }
看完上述內(nèi)容,你們對(duì)如何編寫(xiě)可測(cè)試的Go語(yǔ)言代碼有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。