單元測試中,經常需要mock。
例如,一個函數中,需要調用網絡連接函數建立連接。做單元測試時,這個建立連接的函數就可以mock一下,而不真正去嘗試建立連接。
mock 有時也稱為“打樁”。
例如,mock一個函數,可以說,為一個函數打樁。
在golang中,
gomonkey 就是這樣的工具庫。
本文主要介紹使用gomonkey進行mock。
1.安裝
$ go get github.com/agiledragon/gomonkey
2.使用方法
2.1 mock一個函數
下面例子中,調用鏈是:Compute()--> networkCompute()。本地單測時,一般不會建立網絡連接,因此需要mock netWorkCompute()。
//compute_test.go
package main
import (
"testing"
"github.com/agiledragon/gomonkey"
)
func networkCompute(a, b int) (int, error){
// do something in remote computer
c := a + b
return c, nil
}
func Compute(a, b int)(int, error) {
sum, err := networkCompute(a, b)
return sum, err
}
func TestCompute(t *testing.T) {
patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int,error){
return 2, nil
})
defer patches.Reset()
sum, err := Compute(1, 1)
if sum != 2 || err != nil {
t.Errorf("expected %v, got %v", 2, sum)
}
}
output:
go test -v compute_test.go
=== RUN TestCompute
--- PASS: TestCompute (0.00s)
PASS
ok command-line-arguments 0.006s
上面代碼中,我們mock 了 networkCompute(),返回了計算結果2。
再例如:
下面代碼中,調用鏈:Convert2Json() --> json.Marshal()
嘗試mock json.Marshal()。
// json_test.go
package j
import (
"testing"
"encoding/json"
"github.com/agiledragon/gomonkey"
)
type Host struct {
IP string
Name string
}
func Convert2Json(h *Host) (string, error){
b, err := json.Marshal(h)
return string(b), err
}
func TestConvert2Json(t *testing.T) {
patches := gomonkey.ApplyFunc(json.Marshal, func(v interface{}) ([]byte,error){
return []byte(`{"IP":"192.168.23.92","Name":"Sky"}`), nil
})
defer patches.Reset()
h := Host{Name: "Sky", IP: "192.168.23.92"}
s, err := Convert2Json(&h)
expectedString := `{"IP":"192.168.23.92","Name":"Sky"}`
if s != expectedString || err != nil {
t.Errorf("expected %v, got %v", expectedString, s)
}
}
output:
go test -v json_test.go
=== RUN TestConvert2Json
--- PASS: TestConvert2Json (0.00s)
PASS
ok command-line-arguments 0.006s
2.2 mock 一個方法
例子中,定義一個類型,類型中有兩個方法Compute(), NetworkCompute(),調用關系為:Compute()-->NetworkCompute()。
//compute_test.go
package c
import (
"reflect"
"testing"
"github.com/agiledragon/gomonkey"
)
type Computer struct {
}
func(t *Computer) NetworkCompute(a, b int) (int, error){
// do something in remote computer
c := a + b
return c, nil
}
func(t *Computer) Compute(a, b int)(int, error) {
sum, err := t.NetworkCompute(a, b)
return sum, err
}
func TestCompute(t *testing.T) {
var c *Computer
patches := gomonkey.ApplyMethod(reflect.TypeOf(c), "NetworkCompute",func(_ *Computer, a,b int) (int,error) {
return 2, nil
})
defer patches.Reset()
cp := &Computer{}
sum, err := cp.Compute(1, 1)
if sum != 2 || err != nil {
t.Errorf("expected %v, got %v", 2, sum)
}
}
output:
go test -v compute_test.go
=== RUN TestCompute
--- PASS: TestCompute (0.00s)
PASS
ok command-line-arguments 0.006s
2.3 mock 一個全局變量
例子中,mock一個全局變量。
// g_test.go
package g
import (
"testing"
"github.com/agiledragon/gomonkey"
)
var num = 10
func TestGlobalVar(t *testing.T){
patches := gomonkey.ApplyGlobalVar(&num, 12)
defer patches.Reset()
if num != 12 {
t.Errorf("expected %v, got %v", 12, num)
}
}
output:
go test -v g_test.go
=== RUN TestGlobalVar
--- PASS: TestGlobalVar (0.00s)
PASS
ok command-line-arguments 0.006s
2.4 mock 一個函數序列
函數序列主要用在,一個函數被多次調用,每次調用返回不同值。
// compute_test.go
package g
import (
"testing"
"github.com/agiledragon/gomonkey"
)
func compute(a, b int) (int, error){
return a+b, nil
}
func TestFunc(t *testing.T){
info1 := "2"
info2 := "3"
info3 := "4"
outputs := []gomonkey.OutputCell{
{Values: gomonkey.Params{info1, nil}},// 模擬函數的第1次輸出
{Values: gomonkey.Params{info2, nil}},// 模擬函數的第2次輸出
{Values: gomonkey.Params{info3, nil}},// 模擬函數的第3次輸出
}
patches := gomonkey.ApplyFuncSeq(compute, outputs)
defer patches.Reset()
output, err := compute(1,1)
if output != 2 || err != nil {
t.Errorf("expected %v, got %v", 2, output)
}
output, err = compute(1,2)
if output != 3 || err != nil {
t.Errorf("expected %v, got %v", 2, output)
}
output, err = compute(1,3)
if output != 4 || err != nil {
t.Errorf("expected %v, got %v", 2, output)
}
}
output:
go test -v compute_test.go
=== RUN TestFunc
--- PASS: TestFunc (0.00s)
PASS
ok command-line-arguments 0.006s
關於 mock 成員方法序列、函數變量等可以參考github中例子。
有時會遇到mock失效的情況,這個問題一般是內聯導致的。
什么是內聯?
為了減少函數調用時的堆棧等開銷,對於簡短的函數,會在編譯時,直接內嵌調用的代碼。
請看下面的例子,我們嘗試mock IsEnabled()函數。
package main
import (
"testing"
"github.com/agiledragon/gomonkey"
)
var flag bool
func IsEnabled() bool{
return flag
}
func Compute(a, b int) int {
if IsEnabled(){
return a+b
}
return a-b
}
func TestCompute(t *testing.T) {
patches := gomonkey.ApplyFunc(IsEnabled, func() bool{
return true
})
defer patches.Reset()
sum := Compute(1, 1)
if sum != 2 {
t.Errorf("expected %v, got %v", 2, sum)
}
}
output
go test -v compute_test.go
=== RUN TestCompute
TestCompute: compute_test.go:34: expected 2, got 0
--- FAIL: TestCompute (0.00s)
FAIL
FAIL command-line-arguments 0.007s
FAIL
從輸出結果看,測試用例失敗了。
接着,關閉內聯的,再次嘗試:
go test -v -gcflags=-l compute_test.go
=== RUN TestCompute
--- PASS: TestCompute (0.00s)
PASS
ok command-line-arguments 0.006s
單元測試通過。
對於 go 1.10以下版本,可使用-gcflags=-l
禁用內聯,對於go 1.10及以上版本,可以使用-gcflags=all=-l
。但目前使用下來,都可以。
關於gcflags的用法,可以使用 go tool compile --help
查看 gcflags 各參數含義。