Go 語言測試進階教程
你好啊各位碼農們!在這個教程中,我們將介紹一些進階的測試實踐,很多 Go 語言的核心開發人員以及流行的生產級工具都使用到了它們。
我希望這種通過生產上真實使用的案例來講解的方法。能夠給你一些啟示,讓你深入瞭解怎麼去測試你自己的生產級別的 Go 程式。
注意:如果你對如何測試 Go 語言的程式完全不瞭解的話,我建議你先看看之前的教程:an introduction to testing in Go (譯註:Go 語言中文網譯文:Go 測試介紹)
通過表格驅動的測試來實現良好的測試覆蓋率
我們先來看看strings
程式碼包,如果你看一眼src/strings/
目錄裡面的strings_test.go
檔案,你會發現檔案開頭定義了一些陣列。
舉個例子,我們來看看lastIndexTests
,它是一個IndexTest
型別的陣列:
var lastIndexTests = []IndexTest{ {"", "", 0}, {"", "a", -1}, {"", "foo", -1}, {"fo", "foo", -1}, {"foo", "foo", 0}, {"foo", "f", 0}, {"oofofoofooo", "f", 7}, {"oofofoofooo", "foo", 7}, {"barfoobarfoo", "foo", 9}, {"foo", "", 3}, {"foo", "o", 2}, {"abcABCabc", "A", 3}, {"abcABCabc", "a", 6}, }
這個陣列用來測試strings.go
檔案裡面的LastIndex
函式,裡面有一系列正確的和錯誤的測試用例。
陣列的每個元素由一個字串、分隔符和一個out
整陣列成,它的結構如下:
type IndexTest struct { sstring sep string out int }
這些測試由TestLastIndex()
函式觸發,它會把所有這些測試用例都遍歷一遍,檢查lastIndex
函式的返回值與陣列中事先定義的期待的目標值是否一致。
同樣的實踐在無數的函式中使用過。這個實踐能夠幫助我們確保函式的程式碼發生改動時,函式的預期行為不會發生變化。
使用 testdata 目錄
在某些情況下,你沒辦法像上述的例子一樣,用陣列的形式來指定你期待的測試輸入與輸出。比如說你想要測試在檔案系統上讀寫檔案,或想要測試解析某些特定格式的資料檔案等等。
這個時候,你可以選擇建立一個testdata
目錄,然後把你要用於測試的檔案儲存的那個目錄中。
在標準庫的src/archive/tar/
目錄裡面有一個testdata
目錄。它包含了一些.tar
檔案,用來進行測試。
你可以在reader_test.go
檔案中看到一些較為複雜的例子:
func TestReader(t *testing.T) { vectors := []struct { filestring// Test input file headers []*Header // Expected output headers chksums []string// MD5 checksum of files, leave as nil if not checked errerror// Expected error to occur }{{ file: "testdata/gnu.tar", headers: []*Header{{ Name:"small.txt", Mode:0640, Uid:73025, Gid:5000, Size:5, ModTime:time.Unix(1244428340, 0), Typeflag: '0', Uname:"dsymonds", Gname:"eng", Format:FormatGNU, }, { Name:"small2.txt", Mode:0640, Uid:73025, Gid:5000, Size:11, ModTime:time.Unix(1244436044, 0), Typeflag: '0', Uname:"dsymonds", Gname:"eng", Format:FormatGNU, }}, chksums: []string{ "e38b27eaccb4391bdec553a7f3ae6b2f", "c65bd2e50a56a2138bf1716f2fd56fe9", }, }, // 更多的測試用例
上面的函式中你可以看到 Go 語言的核心開發者使用了我們一開始提到的表格驅動的測試方法以及我們在本節提到的方法。
他們把用於測試的.tar
檔案放在testdata
目錄裡面,然後編寫測試程式碼,確保這些.tar
檔案被解壓出來以後,裡面的檔案和檔案的校驗和與預期的結果一致。
Mock HTTP 請求
當你開始編寫生產級別的 API 和服務的時候,你可能會需要與其他的服務進行互動。能按照你與這些服務的互動方式來進行測試,跟測試你本地的程式碼一樣有必要。
但是,你的互動可能是一個會對資料庫進行 CRUD(譯註:指建立 - 查詢 - 更新 - 刪除)操作的 REST API,當你只是想測試一下這些操作能不夠使用的時候,你肯定不會喜歡這些測試會真正地改動到你的資料庫裡面的資料。
所以,為了解決這個問題,我們可以使用net/http/httptest
來 mock HTTP 的應答:
package main_test import ( "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestHttp(t *testing.T) { // handler := func(w http.ResponseWriter, r *http.Request) { // 我們在這裡面編寫預期的應答,通常情況下 REST API 會 // 返回一個 JSON 字串 io.WriteString(w, "{ \"status\": \"expected service response\"}") } req := httptest.NewRequest("GET", "https://tutorialedge.net", nil) w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) }
上面的測試用例中,我們用我們指定的響應內容,覆蓋了我們請求的 URL 的原本的回覆,然後根據我們自己 mock 的響應內容來繼續測試我們程式的其它部分。
測試程式碼使用獨立的程式碼包
檢視strings_test.go
檔案的前面,你會發現它跟strings.go
檔案並不屬於同一個程式碼包。
為什麼要這樣呢?它可以幫助你避免迴圈匯入。在某些情況下,你需要在你的*_test.go
檔案裡面匯入一些程式碼包來方便你編寫你的測試程式碼,但是如果你匯入的這些包,在你準備測試的包中已經匯入過了,就有可能會產生迴圈依賴。
把單元測試和整合測試區分開
注意:我是從這個文章學習到的這個技巧:Go Advanced Tips Tricks
如果你在為一個大型的企業級 Go 系統編寫測試,那麼你很有可能會有一系列的單元 測試和整合 測試來保證你係統的有效性。
但在通常情況下,你會發現整合測試執行的時間要比單元測試長很多。因為它們可能會接觸到其它的系統。
在這個情況下,我們應該把整合測試的程式碼放到*_integration_test.go
檔案中,
並且這個檔案的頂端新增// +build integration
指令:
// +build integration package main_test import ( "fmt" "testing" ) func TestMainIntegration(t *testing.T) { fmt.Println("My Integration Test") }
這時如果你想要執行這個整合測試,你可以這樣的使用go test
:
$ Go test -tags=integration My Integration Test PASS ok_/Users/elliot/Documents/Projects/tutorials/golang/advanced-go-testing-tutorial 0.006s
結論
在這個教程中,我們討論了一些被 Go 語言開發者所使用的進階的測試技巧。
希望你能從中獲得一些收穫,並且對編寫自己的 Go 測試程式碼能夠有些深入的理解,如果你覺得它有用,或者有任何疑問,請不要猶豫,在評論區給我留言。
注意:如果你想了解更多新文章的諮詢,請關注我的 Twitter@Elliot_F
延伸閱讀
如果你覺得這個文章有點意思,你可以也會喜歡我講解 Go 測試的另外一篇文章:
- Improving Your Tests with Testify in Go (譯註:Go 語言中文網譯文:用 Testify 來改善 GO 測試和模擬 )