Go語言單元測試

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

簡介

Go 語言在設計之初就考慮到了代碼的可測試性。一方面 Go 本身提供了 testing 庫,使用方法很簡單;
另一方面 go 的 package 提供了很多編譯選項,代碼和商務邏輯代碼很容易解耦,可讀性比較強(不妨對比一下C++測試架構)。 本文中,我們討論的重點是 Go 語言中
的單元測試,而且只討論一些基本的測試方法,包括下面幾個方面:

  1. 寫一個簡單的測試案例

  2. Table driven test

  3. 使用輔助測試函數(test helper)

  4. 臨時檔案

這裡我們只涉及到一些通用的測試方法。關於 HTTP server/client 測試,這裡不做深入討論。

閱讀建議

Testing shows the presence, not the absence of bugs -- Edsger W. Dijkstra

在閱讀本文之前,建議您對 Go 語言的 package 有一定的瞭解,並在實際項目中使用過,下面是一些基本的要求:

  1. 瞭解如何在項目中 import 一個外部的package

  2. 瞭解如何將自己的項目按照功能模組劃分 package

  3. 瞭解 struct、struct欄位、函數、變數名字首字母大小寫含義(非必需)

  4. 瞭解一些 Go語言的編譯選項,比如 +build !windows(非必需)

如果你對 1、2都不太瞭解,建議閱讀一下這篇文章How to Write Go Code,動手實踐一下。

寫一個簡單的測試案例

為了便於理解,我們首先給出一個程式碼片段(如果你已經使用過go 的單元測試,可以跳過這個環節):

// demo/equal.gopackage demo// a function to check if two numbers equals to each other.func equal(a, b int) bool {  return a == b}// demo/equal_test.gopackage demoimport (  "testing")func TestEqual(t *testing.T) {  a := 1  b := 1  shouldBe := true  if real := equal(a, b); real == shouldBe {    t.Errorf("equal(%d, %d) should be %v, but is:%v\n", a, b, shouldBe, real)  }}

上面這個例子中,如果你從來沒有使用過單元測試,建議在本地開發環境中運行一次。這裡有幾點需要注意一下:

  1. 這兩個檔案的父目錄必須與包名一致(這裡是 demo),且包名必須是在 $GOPATH 下

  2. 測試案例的函數命名必須符合 TestXXX 格式,並且參數是 t *testing.T

  3. 瞭解一下 t.Errorf 與 t.Fatalf 的行為差異

Table Driven Test

上面的測試案例中,我們一次只能測試一種情況,如果我們希望在一個 TestXXX 函數中進行很多項測試,Table Driven Test 就派上了用場。
舉個例子,假設我們實現了自己的 Sqrt 函數 mymath.Sqrt,我們需要對其進行測試:

首先,我們需要考慮一些特殊情況:

  1. Sqrt(+Inf) = +Inf

  2. Sqrt(±0) = ±0

  3. Sqrt(x < 0) = NaN

  4. Sqrt(NaN) = NaN

然後,我們需要考慮一般情況:

  1. Sqrt(1.0) = 1.0

  2. Sqrt(4.0) = 2.0

  3. ...

注意:在一般情況中,我們對結果進行驗證時,需要考慮小數點精確位元的問題。由於文章篇幅限制,這裡不做額外的處理。

有了思路以後,我們可以基於 Table Driven Test 實現測試案例:

func TestSqrt(t *testing.T) {  var shouldSuccess = []struct {    input    float64 // input    expected float64 // expected result  }{    {math.Inf(1), math.Inf(1)}, // positive infinity    {math.Inf(-1), math.NaN()}, // negative infinity    {-1.0, math.NaN()},    {0.0, 0.0},    {-0.0, -0.0},    {1.0, 1.0},    {4.0, 2.0},  }  for _, ts := range shouldSuccess {    if actual := Sqrt(t.input); actual != ts.expected {      t.Fatalf("Sqrt(%f) should be %v, but is:%v\n", ts.input, ts.expected, actual)    }  }}

輔助函數 (test helper)

在寫測試的過程中,我們可能遇到下面幾個情境:

  1. 待測試的功能需要一些前提條件,比如初始化資料庫連接、開啟檔案、建立資源

  2. 核心功能測試結束後,需要一些清理工作,比如關閉檔案、銷毀資源

  3. 待測試的功能錯誤分類比較多,考慮到table driven test,寫到一個測試函數裡可讀性比較差

這時候,我們需要定義一些輔助函數,以協助核心功能的測試。下面我們以使用者登入校正為例,來看如何使用輔助函數。
我們要測試的函數是 login,為了保證本次單元測試不會汙染資料庫,我們採取的流程是:

  1. 初始化資料庫連接(類似於 Junit 中的 @Before)

  2. 建立一個使用者 (類似於 Junit 中的 @Before)

  3. 測試 login

  4. 刪除該使用者(類似於 Junit 中的 @After)

確定了測試的邏輯以後,我們看下代碼:

// file name: user_test.go// source code: https://github.com/oscarzhao/blogger-server/blob/master/controllers/user_test.go// package level initialization of database connectionsfunc init() {  // init database connections}// testCreateUser 建立一個臨時使用者(test helper)// 具體流程:// 1. mocks a http server// 2. send create user request to the serverfunc testCreateUser(t *testing, userSpec map[string]string) (int, []byte) {  // mock a http server  router := denco.New()  router.Build([]denco.Record{    {"/api/v1/users/:user_id", &route{}},  })  testURL := "/api/v1/users/" + userID  _, params, found := router.Lookup(testURL)  if !found {    t.Fatalf("fails to look up the route, url:%s\n", testURL)  }  handler := func(w http.ResponseWriter, r *http.Request) {    CreateUser(w, r, params)  }  marshaled, _ := json.Marshal(userSpec)  // create request  req, err := http.NewRequest("POST", "http://anything.com", bytes.NewBuffer(marshaled))  if err != nil {    t.Fatalf("should create user success, but fails to send request, error:%s\n", err)  }  // mock ResponseWriter  w := httptest.NewRecorder()  // call create operation  handler(w, req)  return w.Code, w.Body.Bytes()}// testDeleteUser 根據 userID 刪除一個使用者(test helper)func testDeleteUser(t *testing.T, userID string) (int, []byte) {  ...}// TestVerifyLogin 建立使用者、測試登入,然後刪除該使用者// 該函數由 go 語言的 test 架構調用func TestVerifyLogin(t *testing.T) {  userID := uuid.NewV4().String()  data := map[string]string{    "username": "simple_yyxxzz",    "password": "simple_password",    "email":    "not@changed.com",    "phone":    "1234567890",  }  statusCode, msg := testCreateUser(t, userID, data)  if statusCode >= http.StatusBadRequest {    t.Fatalf("should succeeed, create user (%s), but fails, error:%s\n", userID, msg)  }  // 測試結束時,清理資料  defer func(userID string) {    statusCode, msg := testDeleteUser(t, userID)    if statusCode >= http.StatusBadRequest {      t.Errorf("should delete user(%s) successfully, but fails, status code:%d, error:%s\n", userID, statusCode, msg)    }  }(userID)  // 測試登入功能  shouldSuccess := xxx  for _, ts := range shouldSuccess {    statusCode, msg = testVerifyPassword(t, ts)    if statusCode != http.StatusOK {      // if use fatal, user will not be cleaned up      t.Errorf("should verify with %v successfully, but failed, status code:%d, error:%s\n", ts, statusCode, msg)      return    }  }}

在測試代碼中,我們推薦使用 t.Fatalf , 而不是 t.Errorf,一方面測試代碼不需要做太多容錯,另一方面增加了測試代碼的可讀性。

臨時檔案

如果待測試的功能模組涉及到檔案操作,臨時檔案是一個不錯的解決方案。go語言的 ioutil 包提供了 TempDir 和
TempFile 方法,供我們使用。

我們以 etcd 建立 wal 檔案為例,來看一下 TempDir 的用法:

// github.com/coreos/etcd/wal/wal_test.gofunc TestNew(t *testing.T) {  p, err := ioutil.TempDir(os.TempDir(), "waltest")  if err != nil {    t.Fatal(err)  }  defer os.RemoveAll(p)  // 千萬不要忘記刪除目錄  w, err := Create(p, []byte("somedata"))  if err != nil {    t.Fatalf("err = %v, want nil", err)  }  if g := path.Base(w.tail().Name()); g != walName(0, 0) {    t.Errorf("name = %+v, want %+v", g, walName(0, 0))  }  defer w.Close()  // 將檔案 waltest 中的資料讀取到變數 gb []byte 中   // ...  // 根據 "somedata" 產生資料,儲存在變數 wb byte.Buffer 中  // ...  // 臨時檔案中的資料(gb)與 產生的資料(wb)進行對比  if !bytes.Equal(gd, wb.Bytes()) {    t.Errorf("data = %v, want %v", gd, wb.Bytes())  }}

上面這段代碼是從 etcd 中摘取出來的,源碼查看 coreos/etcd - Github。
需要注意的是,使用 TempDir 和 TempFile 建立檔案以後,需要自己去刪除。

關於 package

在寫單元測試時,一般情況下,我們將功能代碼和測試代碼放到同一個目錄下,僅以尾碼 _test 進行區分。

對於複雜的大型項目,功能依賴比較多時,通常在跟目錄下再增加一個 test 檔案夾,不同的測試
放到不同的子目錄下面,如所示:

針對自己的項目進行測試時,可以結合這兩種方式實現測試案例,提高代碼的可讀性和可維護性。

相關連結:

  1. golang.org/pkg/testing

  2. Testing Techniques

  3. Table Driven Test

  4. Learn Testing

掃碼關注公眾號“深入Go語言”

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.