第一個測試 “Hello Test!”
首先,在我們$GOPATH/src目錄下創(chuàng)建hello目錄,作為本文涉及到的所有示例代碼的根目錄。
然后,新建名為hello.go的文件,定義一個函數(shù)hello()
,功能是返回一個由若干單詞拼接成句子:
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.
}
最后,打開終端,進入hello目錄,輸入go test
命令并回車,可以看到如下輸出:
編寫測試代碼
Golang的測試代碼位于某個包的源代碼中名稱以_test.go結(jié)尾的源文件里,測試代碼包含測試函數(shù)、測試輔助代碼和示例函數(shù);測試函數(shù)有以Test開頭的功能測試函數(shù)和以Benchmark開頭的性能測試函數(shù)兩種,測試輔助代碼是為測試函數(shù)服務(wù)的公共函數(shù)、初始化函數(shù)、測試數(shù)據(jù)等,示例函數(shù)則是以Example開頭的說明被測試函數(shù)用法的函數(shù)。
大部分情況下,測試代碼是作為某個包的一部分,意味著它可以訪問包中不可導出的元素。但在有需要的時候(如避免循環(huán)依賴)也可以修改測試文件的包名,如package hello的測試文件,包名可以設(shè)為package hello_test。
功能測試函數(shù)
功能測試函數(shù)需要接收*testing.T類型的單一參數(shù)t,testing.T 類型用來管理測試狀態(tài)和支持格式化的測試日志。測試日志在測試執(zhí)行過程中積累起來,完成后輸出到標準錯誤輸出。
下面是從Go標準庫摘抄的 testing.T類型的常用方法的用法:
測試函數(shù)中的某條測試用例執(zhí)行結(jié)果與預(yù)期不符時,調(diào)用t.Error()
或t.Errorf()
方法記錄日志并標記測試失敗
# /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()
方法,在某條測試用例失敗后就跳出該測試函數(shù)
# /usr/local/go/src/bytes/reader_test.go
func TestReadAfterBigSeek(t *testing.T) {
r := NewReader([]byte("0123456789"))
if _, err := r.Seek(131+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()
方法,跳過某條測試用例的執(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í)行測試用例的過程中通過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()
標記需要并發(fā)執(zhí)行的測試函數(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()
// ...
}
性能測試函數(shù)
性能測試函數(shù)需要接收*testing.B類型的單一參數(shù)b,性能測試函數(shù)中需要循環(huán)b.N次調(diào)用被測函數(shù)。testing.B 類型用來管理測試時間和迭代運行次數(shù),也支持和testing.T相同的方式管理測試狀態(tài)和格式化的測試日志,不一樣的是testing.B的日志總是會輸出。
下面是從Go標準庫摘抄的 testing.B類型的常用方法的用法:
在函數(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()
}
}
通過 b.StopTimer()
、b.ResetTimer()
、b.StartTimer()
來停止、重置、啟動 時間經(jīng)過和內(nèi)存分配計數(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()
記錄在一個操作中處理的字節(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)
}
}
通過b.RunParallel()
方法和 *testing.PB類型的Next()
方法來并發(fā)執(zhí)行被測對象
# /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)
}
}
})
}
測試輔助代碼
測試輔助代碼是編寫測試代碼過程中因代碼重用和代碼質(zhì)量考慮而產(chǎn)生的。主要包括如下方面:
引入依賴的外部包,如每個測試文件都需要的 testing 包等:
# /usr/local/go/src/log/log_test.go:
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
"testing"
"time"
)
定義多次用到的常量和變量,測試用例數(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源代碼一樣,測試代碼中也能定義init函數(shù),init函數(shù)會在引入外部包、定義常量、聲明變量之后被自動調(diào)用,可以在init函數(shù)里編寫測試相關(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)
}
封裝測試專用的公共函數(shù),抽象測試專用的結(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ù)無需接收參數(shù),但需要使用注釋的 Output: 標記說明示例函數(shù)的輸出值,未指定Output:標記或輸出值為空的示例函數(shù)不會被執(zhí)行。
示例函數(shù)需要歸屬于某個 包/函數(shù)/類型/類型 的方法,具體命名規(guī)則如下:
func Example() { ... } # 包的示例函數(shù)
func ExampleF() { ... } # 函數(shù)F的示例函數(shù)
func ExampleT() { ... } # 類型T的示例函數(shù)
func ExampleT_M() { ... } # 類型T的M方法的示例函數(shù)
# 多示例函數(shù) 需要跟下劃線加小寫字母開頭的后綴
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }
go doc 工具會解析示例函數(shù)的函數(shù)體作為對應(yīng) 包/函數(shù)/類型/類型的方法 的用法。
測試函數(shù)的相關(guān)說明,可以通過go help testfunc來查看幫助文檔。
使用 go test 工具
Golang中通過命令行工具go test來執(zhí)行測試代碼,打開shell終端,進入需要測試的包所在的目錄執(zhí)行 go test,或者直接執(zhí)行go test $pkg_name_in_gopath
即可對指定的包執(zhí)行測試。
通過形如go test github.com/tabalt/...
的命令可以執(zhí)行$GOPATH/github.com/tabalt/
目錄下所有的項目的測試。go test std
命令則可以執(zhí)行Golang標準庫的所有測試。
如果想查看執(zhí)行了哪些測試函數(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è)我們有很多功能測試函數(shù),但某次測試只想執(zhí)行其中的某一些,可以通過-run參數(shù),使用正則表達式來匹配要執(zhí)行的功能測試函數(shù)名。如下面指定參數(shù)后,功能測試函數(shù)TestHello不會執(zhí)行到。
[tabalt@localhost hello] go test -v -run=xxx
PASS
ok hello 0.006s
性能測試函數(shù)默認并不會執(zhí)行,需要添加-bench參數(shù),并指定匹配性能測試函數(shù)名的正則表達式;例如,想要執(zhí)行某個包中所有的性能測試函數(shù)可以添加參數(shù)-bench . 或 -bench=.。
[tabalt@localhost hello] go test -bench=.
PASS
BenchmarkHello-8 2000000 657 ns/op
ok hello 1.993s
想要查看性能測試時的內(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可以用來查看我們編寫的測試對代碼的覆蓋率:
詳細的覆蓋率信息,可以通過-coverprofile輸出到文件,并使用go tool cover來查看,用法請參考go tool cover -help
。
更多go test
命令的參數(shù)及用法,可以通過go help testflag
來查看幫助文檔。
高級測試技術(shù)
IO相關(guān)測試
testing/iotest包中實現(xiàn)了常用的出錯的Reader和Writer,可供我們在io相關(guān)的測試中使用。主要有:
觸發(fā)數(shù)據(jù)錯誤dataErrReader,通過DataErrReader()
函數(shù)創(chuàng)建
讀取一半內(nèi)容的halfReader,通過HalfReader()
函數(shù)創(chuàng)建
讀取一個byte的oneByteReader,通過OneByteReader()
函數(shù)創(chuàng)建
觸發(fā)超時錯誤的timeoutReader,通過TimeoutReader()
函數(shù)創(chuàng)建
寫入指定位數(shù)內(nèi)容后停止的truncateWriter,通過TruncateWriter()
函數(shù)創(chuàng)建
讀取時記錄日志的readLogger,通過NewReadLogger()
函數(shù)創(chuàng)建
寫入時記錄日志的writeLogger,通過NewWriteLogger()
函數(shù)創(chuàng)建
黑盒測試
testing/quick包實現(xiàn)了幫助黑盒測試的實用函數(shù) Check和CheckEqual。
Check函數(shù)的第1個參數(shù)是要測試的只返回bool值的黑盒函數(shù)f,Check會為f的每個參數(shù)設(shè)置任意值并多次調(diào)用,如果f返回false,Check函數(shù)會返回錯誤值 *CheckError。Check函數(shù)的第2個參數(shù) 可以指定一個quick.Config類型的config,傳nil則會默認使用quick.defaultConfig。quick.Config結(jié)構(gòu)體包含了測試運行的選項。
# /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ù)是比較給定的兩個黑盒函數(shù)是否相等,函數(shù)原型如下:
func CheckEqual(f, g interface{}, config *Config) (err error)
HTTP測試
net/http/httptest包提供了HTTP相關(guān)代碼的工具,我們的測試代碼中可以創(chuàng)建一個臨時的httptest.Server來測試發(fā)送HTTP請求的代碼:
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)建一個應(yīng)答的記錄器httptest.ResponseRecorder
來檢測應(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())
測試進程操作行為
當我們被測函數(shù)有操作進程的行為,可以將被測程序作為一個子進程執(zhí)行測試。下面是一個例子:
//被測試的進程退出函數(shù)
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
//測試進程退出函數(shù)的測試函數(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)
}
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家學習或者使用Go語言能有所幫助,如果有疑問大家可以留言交流。