/** * 謹獻給Yoyo * * 原文出處:https://www.toptal.com/go/your-introductory-course-to-testing-with-go * @author dogstar.huang <chanzonghuang@gmail.com> 2016-08-11 */
在學習任何新的東西時,具備清醒的頭腦是很重要的。
如果你對Go相當陌生,並來自諸如JavaScript或Ruby這樣的語言,你很可能習慣於使用現成的架構來協助你類比、斷言以及做一些其他測試的 您嘲笑,斷言,和做其他測試巫術。
現在,消除基于于外部依賴或架構的想法。幾年前在學習這門出眾的程式設計語言時,測試是我遇到的第一個障礙,那時只有相當少的一些資源可用。
現在我知道了,在GO中測試成功,意味著對依賴輕裝上陣(如同和GO所有事情那樣),最少依賴於外部類庫,以及編寫更好、可重用的代碼。此Blake Mizerany的經驗介紹敢於向第三方測試庫嘗試,是一個調整你思想很好的開始。你將看到一些關於使用外部類庫以及“Go的方式”的架構的爭論。
想學Go嗎。來看看我們Golang的入門教程吧。
構建自己的測試架構和類比的概念似乎違反直覺,但也可以很容易就能想到,對於學習這門語言,這是一個良好的起點。另外,不像我當時學習那樣,在貫穿常見測試指令碼以及介紹我認為是有效測試和保持代碼整潔最佳實務的過程中,你都有這篇文章作為引導。
以“Go的方式”來做事,消除對外部架構的依賴。 Go中的表格測試
基本的測試單元 - “單元測試”的名聲 - 可以是一個程式的任何部分,它以最簡單的形式,只需要一個輸入並返回一個輸出。讓我們來看一個將要為其編寫測試的簡單函數。顯然,它遠不是完美和完成的,但出於示範目的是足夠好的了:
avg.go
func Avg(nos ...int) int { sum := 0 for _, n := range nos { sum += n } if sum == 0 { return 0 } return sum / len(nos)}
上面函數,func Avg(nos ...int),返回零或給它一系列數位整數平均值。現在讓我們來給它寫一個測試吧。
在Go中,給測試檔案命名和包含待測試代碼的檔案相同名稱,並帶上附加的尾碼_test被當為最佳實務。例如,上面代碼是一個名為avg.go的檔案中,所以我們的測試檔案將被命名為avg_test.go。
注意,這些樣本只是實際檔案的摘錄,因為包定義和匯入出於簡化已刪去。
這是針對Avg函數的測試:
avg_test.go
func TestAvg(t *testing.T) { for _, tt := range []struct { Nos []int Result int }{ {Nos: []int{2, 4}, Result: 3}, {Nos: []int{1, 2, 5}, Result: 2}, {Nos: []int{1}, Result: 1}, {Nos: []int{}, Result: 0}, {Nos: []int{2, -2}, Result: 0}, } { if avg := Average(tt.Nos...); avg != tt.Result { t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg) } }}
關於函數定義,有幾件事情需要注意的: 首先,在測試函數名稱的“Test”首碼。這是必需的,以便工具把它作為一種有效測試檢測出來。 測試函數名稱的後半部分通常是待測試函數或者方法的名稱,在這裡是Avg。 我們還需要傳入稱為testing.T的測試結構,其允許控制測試流。有關此API的更多詳細資料,請訪問此文檔。
現在,讓我們來聊聊這個例子編寫的格式。一個測試套件(一系列測試)正通過Agv()函數運行,並且每個測試含一個特定的輸入和預期的輸出。在我們的例子中,每次測試傳入一系列整數(Nos)和所期望的一個特定的傳回值(Result)。
表格測試從它的結構得名,很容易被表示成一個有兩列的表格:輸入變數和預期的輸出變數。 Golang介面類比
Go語言所提供的最偉大和最強大的功能稱為介面。除了在進行程式架構設計時獲得介面的強大功能和靈活性外,介面也為我們提供了令人驚訝的機會來解耦組件以及在交匯點全面測試他們。
介面是指定方法的集合,也是一個變數類型。
讓我們看一個虛構的情境,假設需要從io.Reader讀取前N個位元組,並把它們作為一個字串返回。它看起來像是這樣:
readn.go
// readN reads at most n bytes from r and returns them as a string.func readN(r io.Reader, n int) (string, error) { buf := make([]byte, n) m, err := r.Read(buf) if err != nil { return "", err } return string(buf[:m]), nil}
顯然,主要要測試的是readN這個功能,當給定各種輸入時,返回正確的輸出。這可以用表格測試來完成。但另外也有兩個特殊的情境該覆蓋到,那就是要檢查: readN被一個大小為n的緩衝調用 readN返回一個錯誤如果拋出異常
為了知道傳遞給r.Read的緩衝區的大小,以及控制它返回的錯誤,我們需要類比傳遞給readN的r。如果看一下Go文檔中的Reader類型,我們看到io.Reader看起來像:
type Reader interface { Read(p []byte) (n int, err error)}
這似乎相當容易。為了滿足io.Reader我們所需要做是有自已類比一個Read方法。所以,ReaderMock可以是這樣:
type ReaderMock struct { ReadMock func([]byte) (int, error)}func (m ReaderMock) Read(p []byte) (int, error) { return m.ReadMock(p)}
我們稍微來分析一下上面的代碼。任何ReaderMock的執行個體明顯地滿足了io.Reader介面,因為它實現了必要的Read方法。我們的類比還包含欄位ReadMock,使我們能夠設定類比方法確切的行為,這使得動態執行個體任何需要的行為非常容易。
為確保介面在運行時能滿足需要,一個偉大的不消耗記憶體的技巧是,把以下代碼插入到我們的代碼中:
var _ io.Reader = (*MockReader)(nil)
這樣會檢查斷言,但不會分配任何東西,這讓我們確保該介面在編譯時間已被正確實現,即在該程式真正使用它運行到任何功能之前。可選的技巧,但很實用。
繼續往前,讓我們來寫第一個測試:r.Read被大小為n的緩衝區調用。為了做到這點,我們傅用了ReaderMock,如下:
func TestReadN_bufSize(t *testing.T) { total := 0 mr := &MockReader{func(b []byte) (int, error) { total = len(b) return 0, nil }} readN(mr, 5) if total != 5 { t.Fatalf("expected 5, got %d", total) }}
正如你在上面看到的,我們通過一個局部變數定義了“假的”io.Reader功能,這可用於後面斷言我們的測試的有效性。相當容易。
再來看下需要測試的第二個情境,這要求我們類比Read以返回一個錯誤:
func TestReadN_error(t *testing.T) { expect := errors.New("some non-nil error") mr := &MockReader{func(b []byte) (int, error) { return 0, expect }} _, err := readN(mr, 5) if err != expect { t.Fatal("expected error") }}
在上面的測試,不管什麼調用了mr.Read(我們類比的Reader)都將返回既定義的錯誤,因此假設readN的正常運行也會這樣做是可靠的。 Golang方法類比
通常我們不需要類比方法,因為取而代之,我們傾向於使用結構和介面。這些更容易控制,但偶爾會碰到這種必要性,我經常看到圍繞這塊話題的困惑。甚至有人問怎麼類比類似log.Println這樣的東西。雖然很少需要測試給log.Println的輸入的情況,我們將利用這次機會來證明。
考慮以下簡單的if語句,根據n的值輸出記錄:
func printSize(n int) { if n < 10 { log.Println("SMALL") } else { log.Println("LARGE") }}
在上面的例子中,我們假設這樣一個可笑的情境:特定測試log.Println被正確的值調用。為了類比這個功能,首先需要把它封裝起來:
var show = func(v ...interface{}) { log.Println(v...)}
以這種方式聲方法 - 作為一個變數 - 允許我們在測試中覆蓋它,並為其分配任何我們所希望的行為。間接地,把log.Println的程式碼替換成show,那麼我們的程式將變成:
func printSize(n int) { if n < 10 { show("SMALL") } else { show("LARGE") }}
現在我們可以測試了:
func TestPrintSize(t *testing.T) { var got string oldShow := show show = func(v ...interface{}) { if len(v) != 1 { t.Fatalf("expected show to be called with 1 param, got %d", len(v)) } var ok bool got, ok = v[0].(string) if !ok { t.Fatal("expected show to be called with a string") } } for _, tt := range []struct{ N int Out string }{ {2, "SMALL"}, {3, "SMALL"}, {9, "SMALL"}, {10, "LARGE"}, {11, "LARGE"}, {100, "LARGE"}, } { got = "" printSize(tt.N) if got != tt.Out { t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got) } } // careful though, we must not forget to restore it to its original value // before finishing the test, or it might interfere with other tests in our // suite, giving us unexpected and hard to trace behavior. show = oldShow}
我們不應該“類比log.Println”,但在那些非常偶然的情況下,當我們出於正當理由真的需要類比一個包級的方法時,為了做到這一點唯一的方法(據我所知)是把它聲明為一個包級的變數,這樣我們就可以控制它的值。
然而,如果我們確實需要類比像log.Println這樣的東西,假如使用了自訂的記錄器,我們可以編寫一個更優雅的解決方案。 Golang模板渲染測試
另一個相當常見的情況是,根據預期測試某個渲染模板的輸出。讓我們考慮一個對http://localhost:3999/welcome?name=Frank的GET請求,它會返回以下body:
<html> <head><title>Welcome page</title></head> <body> <h1 class="header-name"> Welcome <span class="name">Frank</span>! </h1> </body></html>
如果現在它明顯不夠,查詢參數name與類為name的span標籤相匹配,這不是一個巧合。在這種情況下,明顯的測試應該驗證每次跨越多層輸出時這種情況都正確發生。在這裡我發現GoQuery類庫非常有用。
GoQuery使用類似jQuery的API查詢HTML結構,是用於測試程式標籤輸出的有效性是必不可少的。
現在用這種方式我們可以編寫我們的測試了:
welcome__test.go
func TestWelcome_name(t *testing.T) { resp, err := http.Get("http://localhost:3999/welcome?name=Frank") if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } doc, err := goquery.NewDocumentFromResponse(resp) if err != nil { t.Fatal(err) } if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" { t.Fatalf("expected markup to contain 'Frank', got '%s'", v) }}
首先,在處理前我們檢查響應狀態代碼是不是200/OK。
我認為,假設上面的程式碼片段的其餘部分是不言自明不會太牽強:我們使用http包來提取URL並根據響應建立一個新的goquery相容文檔,隨後我們會用它來查詢返回的DOM。我們檢查了在h1.header-name裡面span.name封裝文本'弗蘭克'。