Golang測試技術
本篇文章內容來源于Golang核心開發組成員Andrew Gerrand在Google I/O 2014的一次主題分享“Testing Techniques”,即介紹使用Golang開發 時會使用到的測試技術(主要針對單元測試),包括基本技術、高級技術(并發測試、mock/fake、競爭條件測試、并發測試、內/外部測 試、vet工具等)等,感覺總結的很全面,這里整理記錄下來,希望能給大家帶來幫助。原Slide訪問需要自己搭梯子。另外這里也要吐槽一 下:Golang官方站的slide都是以一種特有的golang artical的格式放出的(用這個工具http://go-talks.appspot.com/可以在線觀看),沒法像pdf那樣下載,在國內使用和傳播極其不便。
一、基礎測試技術
1、測試Go代碼
Go語言內置測試框架。
內置的測試框架通過testing包以及go test命令來提供測試功能。
下面是一個完整的測試strings.Index函數的完整測試文件:
//strings_test.go (這里樣例代碼放入strings_test.go文件中)
package strings_test
import (
"strings"
"testing"
)
func TestIndex(t *testing.T) {
const s, sep, want = "chicken", "ken", 4
got := strings.Index(s, sep)
if got != want {
t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)//注意原slide中的got和want寫反了
}
}
$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok command-line-arguments 0.007s
go test的-v選項是表示輸出詳細的執行信息。
將代碼中的want常量值修改為3,我們制造一個無法通過的測試:
$go test -v strings_test.go
=== RUN TestIndex
— FAIL: TestIndex (0.00 seconds)
strings_test.go:12: Index("chicken","ken") = 4; want 3
FAIL
exit status 1
FAIL command-line-arguments 0.008s
2、表驅動測試
Golang的struct字面值(struct literals)語法讓我們可以輕松寫出表驅動測試。
package strings_test
import (
"strings"
"testing"
)
func TestIndex(t *testing.T) {
var tests = []struct {
s string
sep string
out int
}{
{"", "", 0},
{"", "a", -1},
{"fo", "foo", -1},
{"foo", "foo", 0},
{"oofofoofooo", "f", 2},
// etc
}
for _, test := range tests {
actual := strings.Index(test.s, test.sep)
if actual != test.out {
t.Errorf("Index(%q,%q) = %v; want %v",
test.s, test.sep, actual, test.out)
}
}
}
$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok command-line-arguments 0.007s
3、T結構
*testing.T參數用于錯誤報告:
t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)
也可以用于enable并行測試(parallet test):
t.Parallel()
控制一個測試是否運行:
if runtime.GOARCH == "arm" {
t.Skip("this doesn't work on ARM")
}
4、運行測試
我們用go test命令來運行特定包的測試。
默認執行當前路徑下包的測試代碼。
$ go test
PASS
$ go test -v
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
要運行工程下的所有測試,我們執行如下命令:
$ go test github.com/nf/…
標準庫的測試:
$ go test std
注:假設strings_test.go的當前目錄為testgo,在testgo目錄下執行go test都是OK的。但如果我們切換到testgo的上一級目錄執行go test,我們會得到什么結果呢?
$go test testgo
can't load package: package testgo: cannot find package "testgo" in any of:
/usr/local/go/src/pkg/testgo (from $GOROOT)
/Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)
提示找不到testgo這個包,go test后面接著的應該是一個包名,go test會在GOROOT和GOPATH下查找這個包并執行包的測試。
5、測試覆蓋率
go tool命令可以報告測試覆蓋率統計。
我們在testgo下執行go test -cover,結果如下:
go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo
FAIL _/Users/tony/Test/Go/testgo [build failed]
顯然通過cover參數選項計算測試覆蓋率不僅需要測試代碼,還要有被測對象(一般是函數)的源碼文件。
我們將目錄切換到$GOROOT/src/pkg/strings下,執行go test -cover:
$go test -v -cover
=== RUN TestReader
— PASS: TestReader (0.00 seconds)
… …
=== RUN: ExampleTrimPrefix
— PASS: ExampleTrimPrefix (1.75us)
PASS
coverage: 96.9% of statements
ok strings 0.612s
go test可以生成覆蓋率的profile文件,這個文件可以被go tool cover工具解析。
在$GOROOT/src/pkg/strings下面執行:
$ go test -coverprofile=cover.out
會再當前目錄下生成cover.out文件。
查看cover.out文件,有兩種方法:
a) cover -func=cover.out
$sudo go tool cover -func=cover.out
strings/reader.go:24: Len 66.7%
strings/reader.go:31: Read 100.0%
strings/reader.go:44: ReadAt 100.0%
strings/reader.go:59: ReadByte 100.0%
strings/reader.go:69: UnreadByte 100.0%
… …
strings/strings.go:638: Replace 100.0%
strings/strings.go:674: EqualFold 100.0%
total: (statements) 96.9%
b) 可視化查看
執行go tool cover -html=cover.out命令,會在/tmp目錄下生成目錄coverxxxxxxx,比如/tmp/cover404256298。目錄下有一個 coverage.html文件。用瀏覽器打開coverage.html,即可以可視化的查看代碼的測試覆蓋情況。
關于go tool的cover命令,我的go version go1.3 darwin/amd64默認并不自帶,需要通過go get下載。
$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover
下載后,cover安裝在$GOROOT/pkg/tool/darwin_amd64下面。
二、高級測試技術
1、一個例子程序
outyet是一個web服務,用于宣告某個特定Go版本是否已經打標簽發布了。其獲取方法:
go get github.com/golang/example/outyet
注:
go get執行后,cd $GOPATH/src/github.com/golang/example/outyet下,執行go run main.go。然后用瀏覽器打開http://localhost:8080即可訪問該Web服務了。
2、測試Http客戶端和服務端
net/http/httptest包提供了許多幫助函數,用于測試那些發送或處理Http請求的代碼。
3、httptest.Server
httptest.Server在本地回環網口的一個系統選擇的端口上listen。它常用于端到端的HTTP測試。
type Server struct {
URL string // base URL of form http://ipaddr:port with no trailing slash
Listener net.Listener
// TLS is the optional TLS configuration, populated with a new config
// after TLS is started. If set on an unstarted server before StartTLS
// is called, existing fields are copied into the new config.
TLS *tls.Config
// Config may be changed after calling NewUnstartedServer and
// before Start or StartTLS.
Config *http.Server
}
func NewServer(handler http.Handler) *Server
func (*Server) Close() error
4、httptest.Server實戰
下面代碼創建了一個臨時Http Server,返回簡單的Hello應答:
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)
5、httptest.ResponseRecorder
httptest.ResponseRecorder是http.ResponseWriter的一個實現,用來記錄變化,用在測試的后續檢視中。
type ResponseRecorder struct {
Code int // the HTTP response code from WriteHeader
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
}
6、httptest.ResponseRecorder實戰
向一個HTTP handler中傳入一個ResponseRecorder,通過它我們可以來檢視生成的應答。
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())
7、競爭檢測(race detection)
當兩個goroutine并發訪問同一個變量,且至少一個goroutine對變量進行寫操作時,就會發生數據競爭(data race)。
為了協助診斷這種bug,Go提供了一個內置的數據競爭檢測工具。
通過傳入-race選項,go tool就可以啟動競爭檢測。
$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package
注:一個數據競爭檢測的例子
例子代碼:
//testrace.go
package main
import "fmt"
import "time"
func main() {
var i int = 0
go func() {
for {
i++
fmt.Println("subroutine: i = ", i)
time.Sleep(1 * time.Second)
}
}()
for {
i++
fmt.Println("mainroutine: i = ", i)
time.Sleep(1 * time.Second)
}
}
$go run -race testrace.go
mainroutine: i = 1
==================
WARNING: DATA RACE
Read by goroutine 5:
main.func·001()
/Users/tony/Test/Go/testrace.go:10 +0×49
Previous write by main goroutine:
main.main()
/Users/tony/Test/Go/testrace.go:17 +0xd5
Goroutine 5 (running) created at:
main.main()
/Users/tony/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i = 2
mainroutine: i = 3
subroutine: i = 4
mainroutine: i = 5
subroutine: i = 6
mainroutine: i = 7
subroutine: i = 8
8、測試并發(testing with concurrency)
當測試并發代碼時,總會有一種使用sleep的沖動。大多時間里,使用sleep既簡單又有效。
但大多數時間不是”總是“。
我們可以使用Go的并發原語讓那些奇怪不靠譜的sleep驅動的測試更加值得信賴。
9、使用靜態分析工具vet查找錯誤
vet工具用于檢測代碼中程序員犯的常見錯誤:
– 錯誤的printf格式
– 錯誤的構建tag
– 在閉包中使用錯誤的range循環變量
– 無用的賦值操作
– 無法到達的代碼
– 錯誤使用mutex
等等。
使用方法:
go vet [package]
10、從內部測試
golang中大多數測試代碼都是被測試包的源碼的一部分。這意味著測試代碼可以訪問包種未導出的符號以及內部邏輯。就像我們之前看到的那樣。
注:比如$GOROOT/src/pkg/path/path_test.go與path.go都在path這個包下。
11、從外部測試
有些時候,你需要從被測包的外部對被測包進行測試,比如測試代碼在package foo_test下,而不是在package foo下。
這樣可以打破依賴循環,比如:
– testing包使用fmt
– fmt包的測試代碼還必須導入testing包
– 于是,fmt包的測試代碼放在fmt_test包下,這樣既可以導入testing包,也可以同時導入fmt包。
12、Mocks和fakes
通過在代碼中使用interface,Go可以避免使用mock和fake測試機制。
例如,如果你正在編寫一個文件格式解析器,不要這樣設計函數:
func Parser(f *os.File) error
作為替代,你可以編寫一個接受interface類型的函數:
func Parser(r io.Reader) error
和bytes.Buffer、strings.Reader一樣,*os.File也實現了io.Reader接口。
13、子進程測試
有些時候,你需要測試的是一個進程的行為,而不僅僅是一個函數。例如:
func Crasher() {
fmt.Println("Going down in flames!")
os.Exit(1)
}
為了測試上面的代碼,我們將測試程序本身作為一個子進程進行測試:
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)
}
來自:http://tonybai.com/2014/10/22/golang-testing-techniques/