編寫測試友好的Golang代碼
目前我們有大量的應用采用了Golang程序進行構建,但是在執行研發流程里我們會發現一些來自于靜態編譯程序的不便:相對于我們之前使用的Python語言程序而言,我們無法在程序功能的單元測試里大量的使用Mock方式來進行高效測試。
而這些東西往往可以在開發人員編寫單元測試用例時有效的節省時間和一些額外的環境準備成本。因此,這也給我們的程序的單元覆蓋率帶來了很多麻煩的地方:一些依賴于額外驗證和表現的情況或者小幾率出現的情況需要復雜的模擬步驟,對開發進度和效率帶來了一些額外的影響。如何編寫一個測試友好的Golang程序成為一個無法繞開的問題。
從動態語言到靜態語言
動態語言有良好的運行時修改屬性,在運行時的動態修改函數,可以進行有效的Mock。比如在Python
(以3為例,內置了unittest.mock標準庫)程序中:
with patch.object(ProductionClass, 'method', return_value=None) as mock_method:
thing = ProductionClass()
thing.method(1, 2, 3)
自然而然的,我們想到了這樣的用法:
var imp = func() bool {
return true
}
func TestFunc(t *testing.T) {
defer func(org func() bool) {
imp = org
}(imp)
img = func() bool {
return false
}
// testing or something else...
}
這樣實現Mock是完全可以的,但是實際上會帶來一些額外的問題,比如說在MVC框架中,我們正常采用的方式一般是這樣的:
import (
"models"
...
)
func A(ctx Context) error {
...
data := models.Data()
...
}
這種方式則是無法在運行中進行動態Mock的,除非將其轉換為參數方式進行調用。
func TestFunc(t *testing.T) {
Convey("test", t, func() {
defer func(org func() string) {
models.Data = org // Error: cannot assign to models.Data
}(models.Data)
models.Data = func() string {
return "mocked!"
}
....
})
}
轉成
// var data = models.Data
// in A: data := data()
func TestFunc(t *testing.T) {
Convey("test", t, func() {
defer func(org func() string) {
data = org
}(data)
data = func() string {
return "mocked!"
}
})
}
這樣寫法略微會多處大量的臨時函數指針變量,如果是使用這種方式則需要額外的變量值的對應關系,測試完成后變量值需要恢復成原有指針(如果需要測試正常功能)。
從變量到接口
除了上面介紹的方法以外,是不是還有看起來稍微優雅一點的測試方法呢?我們嘗試將上面的函數形式換成下面的接口形式,將interface對應的變量作為全局變量。
// main.go
var fetcher DataFetcherInterface
type DataFetcherInterface interface {
Data() string
}
type DataFetcher struct {
}
func (d DataFetcher) Data() string {
return "hello world!"
}
func Func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", fetcher.Data())
}
func main() {
fetcher = DataFetcher{}
http.HandleFunc("/", Func)
http.ListenAndServe("127.0.0.1:12821", nil)
}
這樣的話我們就可以在測試文件里面定義一個FakeDataFetcher,實現相關的功能:
// main_test.go
type FakeDataFetcher struct {
}
func (f FakeDataFetcher) Data() string {
return "mocked!"
}
func TestFunc(t *testing.T) {
Convey("test", t, func() {
defer func(org DataFetcherInterface) {
fetcher = org
}(fetcher)
fetcher = FakeDataFetcher{}
req, _ := http.NewRequest("GET", "http://example.com/", nil)
w := httptest.NewRecorder()
Func(w, req)
So(w.Body.String(), ShouldEqual, "mocked!")
})
}
這樣可以減少變量的生成個數,同時,也可以通過FakeDataFetcher{}
傳入不同的參數,實現不同的Faker測試。值得注意的是,在這個interface方法中需要特別注意變量共享的線程安全問題。
依賴注入
上面兩種方法似乎思路類似,除了這些方案之外,還有沒有其他的方案呢?最后介紹一下依賴注入的方式,這種方式也可以與上面提到的接口方式搭配使用。這種方式實現起來比較簡單方便,也非常適合利用在一些面向過程場景中。
// main.go
type EchoInterface interface {
Echo() string
}
type Echoer struct {
}
func (e Echoer) Echo() string {
return "hello world!"
}
func Echo(e EchoInterface) string {
return e.Echo()
}
func main() {
provider := Echoer{}
fmt.Println(Echo(provider))
}
測試文件:
// main_test.go
type FakeEchoer struct {
}
func (f FakeEchoer) Echo() string {
return "mocked!"
}
func TestFunc(t *testing.T) {
Convey("test", t, func() {
provider := FakeEchoer{}
So(Echo(provider), ShouldEqual, "mocked!")
})
}
總結
上面的幾種測試方法基本上是通過固定的原型將代碼轉為測試友好的Golang代碼。這樣可以通過Mock,減少來自于其他數據和前置條件的影響,盡可能的降低代碼開發的附加成本。
來自: http://ipfans.github.io/2016/04/writing-testable-golang-code/