Go 語言基礎:Test 的基礎知識

本文內容:

  1. 功能測試

  2. 性能測試

  3. Main 測試

  4. 子測試

  5. 示例文件

  6. 跳過函數

使用go test命令將會自動執行所有的形如func TestXxx(*testing.T)的測試函數。

在測試函數中,使用 Error,Fail 或者相關方法來表示測試失敗。

測試文件與源代碼文件放在一塊,測試文件的文件名以_test.go結尾。

測試文件不會被正常編譯,只會在使用go test命令時編譯。

測試用例名稱爲Test加上待測試的方法名。

功能測試

func TestAbs(t *testing.T) {
  got := Abs(-1)
  if got != 1 {
    t.Errorf("Abs(-1) = %d; want 1", got)
  }
}

TCP 功能測試

假設需要測試某個 API 接口的 handler 能夠正常工作,例如 helloHandler

func helloHandler(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("hello world"))
}

那我們可以創建真實的網絡連接進行測試:

import (
       "io/ioutil"
       "net"
       "net/http"
       "testing"
)
func handleError(t *testing.T, err error) {
       t.Helper()
       if err != nil {
               t.Fatal("failed", err)
      }
}
func TestConn(t *testing.T) {
       ln, err := net.Listen("tcp", "127.0.0.1:0")
       handleError(t, err)
       defer ln.Close()
       http.HandleFunc("/hello", helloHandler)
       go http.Serve(ln, nil)
       resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
       handleError(t, err)
       defer resp.Body.Close()
       body, err := ioutil.ReadAll(resp.Body)
       handleError(t, err)
       if string(body) != "hello world" {
               t.Fatal("expected hello world, but got", string(body))
      }
}

HTTP 測試

針對 http 開發的場景,使用標準庫 net/http/httptest 進行測試更爲高效。

上述的測試用例改寫如下:

import (
       "io/ioutil"
       "net/http"
       "net/http/httptest"
       "testing"
)
func TestConn(t *testing.T) {
       req := httptest.NewRequest("GET", "http://example.com/foo", nil)
       w := httptest.NewRecorder()
       helloHandler(w, req)
       bytes, _ := ioutil.ReadAll(w.Result().Body)
       if string(bytes) != "hello world" {
               t.Fatal("expected hello world, but got", string(bytes))
      }
}

使用 httptest 模擬請求對象 (req) 和響應對象(w),達到了相同的目的。

性能測試

形如func BenchmarkXxx(*testing.B)的測試函數。

使用go test -bench命令運行。

func BenchmarkRandInt(b *testing.B) {
  for i := 0; i < b.N; i++ {
     rand.Int()
  }
}

性能測試函數必須循環運行目標代碼b.N次。

在性能測試的過程中,b.N的值會自動調整,直至性能測試函數執行的時長達到一個可靠的時間。

性能測試將會輸出:BenchmarkRandInt-8 68453040 17.8 ns/op

意爲:循環了 68453040 次,平均每次循環使用了 17.8ns

重置計時器

如果在執行目標代碼前需要進行一些耗時的鋪墊,則如要在循環前重置計時器。

func BenchmarkBigLen(b *testing.B) {
  big := NewBig()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
     big.Len()
  }
}

並行測試

如果性能測試需要使用並行設置,則需要使用RunParallel()輔助函數。

這種情況下需要使用go test -cpu來運行性能測試。

func BenchmarkTemplateParallel(b *testing.B) {
  templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
  b.RunParallel(func(pb *testing.PB) {
     var buf bytes.Buffer
     for pb.Next() {
        buf.Reset()
        templ.Execute(&buf, "World")
    }
  })
}

Main 測試

有的時候,需要在測試前進行相關設置,或在測試後進行清理工作。還有的時候,需要控制哪些代碼在主線程上運行。爲了支持這些情況,可以提供一個主測試函數:fun TestMain(m *testing.M)

在一個測試文件中包含TestMain(m)之後,測試時將只會調用TestMain(m),而不會直接運行其他的測試函數。TestMain(m)運行在主協程當中並且可以在m.Run()前後編寫任何必要的設置或清理代碼。m.Run()將會返回一個退出碼給os.Exit。如果TestMain運行結束,m.Run()的結果將會被傳遞給os.Exit

flag

運行TestMain的時候,flag.Parse還沒有被運行,如果有需要,應該顯式地調用flag.Parse方法。不然,命令行的flags總是在各個功能測試或者性能測試的時候被解析。

func TestMain(m *testing.M) {
  // call flag.Parse() here if TestMain uses flags
  os.Exit(m.Run())
}

子測試

TBRun()方法可以直接執行子功能測試和子性能測試,而不用爲每一個測試用例單獨編寫一個測試函數。這樣便可以使用類似於表驅動基準測試和創建分層測試。這種方法也提供了一種方式來處理共同的

func TestFoo(t *testing.T) {
  // <setup code>
  t.Run("A=1", func(t *testing.T) { ... })
  t.Run("A=2", func(t *testing.T) { ... })
  t.Run("B=1", func(t *testing.T) { ... })
  t.Run("A=1", func(t *testing.T) { ... })
  // <tear-down code>
}

每一個子功能測試和子性能測試都有唯一的名字:頂層的測試函數的名字和傳入Run()方法的字符串用/連接,後面再加上一個可選的用於消除歧義的字符串。上面的四個子測試的唯一名字是:

命令行中的-run參數和-bench參數是可選的,可用來匹配測試用例的名字。對於包含多個斜槓分隔元素的測試,例如 subtests,參數本身是斜槓分隔的,表達式依次匹配每個 name 元素。由於是可選參數,因此空表達式匹配所有的字符串。

go test -run ''      # Run all tests.
go test -run Foo     # Run top-level tests matching "Foo", such as "TestFooBar".
go test -run Foo/A=  # For top-level tests matching "Foo", run subtests matching "A=".
go test -run /A=1    # For all top-level tests, run subtests matching "A=1".

並行測試

子測試還可用於控制並行測試。只有當所有的子測試完成,父測試才能完成一次。在這個例子裏,所有測試都是相互並行運行的,且僅相互並行運行,而不考慮可能定義的其他頂級測試。

func TestGroupedParallel(t *testing.T) {
  for _, tc := range tests {
     tc := tc // capture range variable
     t.Run(tc.Name, func(t *testing.T) {
        t.Parallel()
        ...
    })
  }
}

如果程序超過了 8192 個並行的 goroutine,競態檢測器就會終止它,因此,在運行設置了 - race 標誌的並行測試時要小心。

在並行子測試完成之前,Run 不會返回,這提供了一種在一組並行測試之後進行清理的方法。

func TestTeardownParallel(t *testing.T) {
  // This Run will not return until the parallel tests finish.
  t.Run("group", func(t *testing.T) {
     t.Run("Test1", parallelTest1)
     t.Run("Test2", parallelTest2)
     t.Run("Test3", parallelTest3)
  })
  // <tear-down code>
}

幫助函數

Helper()函數將當前所在的函數標記爲測試幫助方法。當打印文件和代碼行信息時,該方法會被跳過。

package main
import "testing"
type calcCase struct{ A, B, Expected int }
func createMulTestCase(t *testing.T, c *calcCase) {
       // t.Helper()
       if ans := Mul(c.A, c.B); ans != c.Expected {
               t.Fatalf("%d * %d expected %d, but %d got",
                       c.A, c.B, c.Expected, ans)
      }
}
func TestMul(t *testing.T) {
       createMulTestCase(t, &calcCase{2, 3, 6})
       createMulTestCase(t, &calcCase{2, -3, -6})
       createMulTestCase(t, &calcCase{2, 0, 1}) // wrong case
}

在這裏,我們故意創建了一個錯誤的測試用例,運行 go test,用例失敗,會報告錯誤發生的文件和行號信息:

$ go test
--- FAIL: TestMul (0.00s)
   calc_test.go:11: 2 * 0 expected 1, but 0 got
FAIL
exit status 1
FAIL    example 0.007s

可以看到,錯誤發生在第 11 行,也就是幫助函數 createMulTestCase 內部。18, 19, 20 行都調用了該方法,我們第一時間並不能夠確定是哪一行發生了錯誤。有些幫助函數還可能在不同的函數中被調用,報錯信息都在同一處,不方便問題定位。因此,Go 語言在 1.9 版本中引入了 t.Helper(),用於標註該函數是幫助函數,報錯時將輸出幫助函數調用者的信息,而不是幫助函數的內部信息。

修改 createMulTestCase,調用 t.Helper()

func createMulTestCase(c *calcCase, t *testing.T) {
   t.Helper()
       t.Run(c.Name, func(t *testing.T) {
               if ans := Mul(c.A, c.B); ans != c.Expected {
                       t.Fatalf("%d * %d expected %d, but %d got",
                               c.A, c.B, c.Expected, ans)
              }
      })
}

運行 go test,報錯信息如下,可以非常清晰地知道,錯誤發生在第 20 行。

$ go test
--- FAIL: TestMul (0.00s)
   calc_test.go:20: 2 * 0 expected 1, but 0 got
FAIL
exit status 1
FAIL    example 0.006s

關於 helper 函數的 2 個建議:

示例文件

測試工具包還能運行和驗證示例代碼。示例函數包含一個結論行註釋,該註釋以Output:開頭,然後比較示例函數的標準輸出和註釋中的內容。

Output

func ExampleHello() {
  fmt.Println("hello")
  // Output: hello
}
func ExampleSalutations() {
  fmt.Println("hello, and")
  fmt.Println("goodbye")
  // Output:
  // hello, and
  // goodbye
}

Unordered output

Unordered output的前綴註釋將匹配任意的行順序。

func ExamplePerm() {
  for _, value := range Perm(5) {
     fmt.Println(value)
  }
  // Unordered output: 4
  // 2
  // 1
  // 3
  // 0
}

不包含output註釋的示例函數,將不會被執行。

爲包聲明示例的命名約定:

func Example() { ... }
func ExampleF() { ... }
func ExampleT() { ... }
func ExampleT_M() { ... }

包 / 類型 / 函數 / 方法的多個示例函數可以通過在名稱後面附加一個不同的後綴來提供。後綴必須以小寫字母開頭。

func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

當整個測試文件包含單個示例函數、至少一個其他函數、類型、變量或常量聲明,且不包含測試或基準函數時,它將作爲示例顯示。

跳過函數

功能測試或性能測試時可以跳過一些測試函數。

func TestTimeConsuming(t *testing.T) {
  if testing.Short() {
     t.Skip("skipping test in short mode.")
  }
  ...
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XtKQY8KImC1r9-o5vFFquQ