Go 語言中的測試

接觸過 java 的同時,junit 這個名稱並不會陌生,或者 python 的測試框架 unittest。可是有習慣寫測試程序的卻聊聊無幾。在 golang 中,關於 testing 的支持比較完善,對於一般的函數測試來說,用不到第三方工具。

官方說明, golang testing https://golang.org/pkg/testing/

要在 go 程序開發過程中使用單元測試程序,無須安裝其它工具,安裝了 golang 環境時已經包含,golang 的工具鏈還是比較完善的。

關於單元測試代碼、測試用例、自動化測試等不是有悟想討論的,不同規模的工程、不同進度的項目要求、不同的管理方法,會讓管理者作出不同的決定。本文僅說說如果在 golang 程序中集成可測試代碼。

要想在 golang 使用 go test 工具鏈來實現代碼單元測試功能,只需要遵守 go test 工具的規範。

本文並未涉及的 golang testing 的所有內容,只會講有悟在寫測試程序用到的功能,完整內容見上面的官方說明鏈接。

以下從如何使用的角度來介紹 go testing,並不是大而全的框架性講解。

按照有悟的慣例,先準備一下示例代碼工程:

~/Projects/go/examples
➜ mkdir hellotest && cd hellotest

~/Projects/go/examples/hellotest
➜ go mod init hellotest

~/Projects/go/examples/hellotest
➜ touch youwutoday.go youwu_test.go
// youwutoday.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello testing...😀")
}
// youwu_test.go
package main_test

組織你的單元測試代碼

爲了更好的管理單元測試代碼,按照 go testing 的命名規範,運行 go testing 會掃描並編譯運行工程目錄下 xxx_test.go 代碼文件中的 TestXXX 測試函數。對一個沒有任何測試代碼的 go 程序工程運行 go testing 只會提示 no tests to run 並不會報錯。

不用擔心這些測試代碼管理會讓你的代碼工程變得混亂,你可以使用一個專門的工程級目錄(比如叫 test) 來存放所有主要的測試代碼。也可以在對應的包(比如 app)下使用對應測試包(比如 app_test)。這樣就可以利用人爲區分,用何種目錄方式完全取決於你的需要。但這對於 go 工具鏈來說不是必要的,因爲它並不會把這些測試代碼打包到程序的發行版本中。

關於 go testing 啓動命令

在上面的例子中,單元測試用例的啓動都是使用 go test 來啓動的,關於這個命令,有必要說明的地方。

go test .

僅從當前目錄查找 xxx_test.go 測試代碼

go test ./...

找出當前的目錄以及子目錄下所的 xxx_test.go 測試代碼

go test ./xxx/xxx

找出路徑下的 xxx_test.go 測試代碼,如果包含子目錄,使用 ./xxx/xxx/...,即在路徑後加上 /...,注意,是 3 個 “.” 號。

go test 路徑 -v

-v 參數,打開終端信息打印的開關。在沒有 -v 選項時,測試代碼中的打印函數(t.Logt.Skipt.Errort.Fatal)向終端打印的信息,只有測試函數狀態爲 FAIL 的纔會顯示。而 fmt.Print 只會在任一測試函數爲 FAIL 時,信息纔會顯示。所以在調試單元測試代碼時,通常會使用 -v 參數,可多次成功執行的,不帶 -v,以保持終端信息提示的清晰。這只是實踐建議,是否使用取決於你。

go test -run ^TestXXXX$

-run 用來指定執行某個 TestXXX 單元測試函數,支持使用正則表達式來匹配測試函數名稱。當然,像 TestMaininit() 這類框架性代碼仍然會被執行,以確保環境正確。

在測試中報錯與跳出

go testing 中,你可能見不到 assertgo testing 框架簡化了這一切的工作。在上面的例子,有悟使用了 t.Log 來打印終端信息。你可以使用 t.Error 來結合 if 判斷來打印信息,並標記這個測試出現錯誤。

// youwu_test.go
func TestReportError(t *testing.T) {
    t.Error("report in: t.Error")
}

會得到類似於下面這樣的錯誤報告:

...
=== RUN   TestReportError
    youwu_test.go:42: report in: t.Error
--- FAIL: TestReportError (0.00s)
...
exit status 1
FAIL    hellotest       0.498s

其實 t.Error 等同於 t.Log + t.Fail,當只標記錯誤不打印信息時,使用t.Fail

跳出單元測試函數的選擇:

t.SkipNow

跳出當前單元測試函數,狀態爲跳出(SKIP,不計爲失敗),執行下一個測試函數

t.Skip(信息)

t.Log -> t.SkipNow 打印並跳出當前單元測試函數

t.Fail

標記當前單元測試函數狀態爲錯誤(FAILED),繼續執行

t.FailNow

標記當前單元測試函數狀態爲錯誤(FAILED)並跳出,執行下一個測試函數

關於報錯機制,有如下幾種:

t.Error(信息)

t.Log(信息) -> t.Fail(),該單元測試會繼續執行

t.Fatal(信息)

t.Log(信息) -> t.FailNow(),跳出該單元測試,執行下一個單元測試函數

以上關於終端打印的函數 t.Logt.Errort.Fatal 都有對應的格式化形式 t.Logft.Errorft.Fatalf,它們的關係就像 fmt.Printfmt.Printf 的關係一樣。

測試程序初始化或清理

類似於 java junit 或者 python unittest,單元測試程序的運行分爲 SetupRuntearDown三個步驟,其中 setup 是用爲 Run 提供環境初始化的過程,比如連接數據庫、數據準備等等,tearDown 則是 Run 之後清理,比如刪除臨時文件、數據庫關閉等操作。

go testing 測試框架中並沒命名 SetuptearDown 函數。當然,你可以利用 go 程序本身的機制或者 go testing 提供的手段。

當然,如果你的單元測試代碼僅僅是一個無須外部數據支持的計算函數,那你根本無須理會初始化或者清理,直接將單元測試代碼編寫爲 xxx_test.goTestXXX 函數即可。

任何 go 代碼程序運行時,當前包、當前代碼文件中的 init() 函數會被最先執行。xxx_test.go 測試代碼的也不例外,那麼可以將初始化代碼放在這個函數內部。

(一個包內有多個初始化函數時,會按一定順序執行。將在另一篇文件中介紹 )

youwu_test.go 中添加

package main_test

import (
    "fmt"
    "testing"
)
func TestWorld(t *testing.T) {
    t.Log("TestWorlds")
}
func init() {
    fmt.Println("hello, i'm \"init\".")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
=== RUN   TestHello
    youwu_test.go:9: TestHello
--- PASS: TestHello (0.00s)
PASS
ok      hellotest       0.316s

測試函數級別:使用 t 實例的 Cleanup 方法註冊一個在單元測試程序退出前執行的函數。包級別:使用 TestMain 模式。

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
=== RUN   TestClean
    youwu_test.go:21: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
...

TestMaingo testing 測試框架的指定函數。用於控制整個測試過程,這個函數是包級別的。即一個包下,如果有多個 xxx_test.go 測試代碼,只能在其中某個 xxx_test.go 中定義。其基本格式大致如下:

在測試代碼中添加:

// youwu_test.go
package main_test

import (
    "fmt"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

// func TestWorld(t *testing.T) {
//  t.Log("TestWorlds")
// }

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

使用 go test -v 運行得到:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:10: TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestClean
    youwu_test.go:22: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
PASS
😀 結束所有測試前
ok      hellotest       0.369s

以上的順序,可以概括爲如下的流程:

`init()` → `TestMain` → TestXXX
                           ↓  
              ↑       ← t.Cleanup

benckmark | 性能基準測試

go testing 除了函數正確性測試之外,還支持性能基準測試(benchmark),即可多次運行,並計算其平均執行時間從而評估其運行效率。網上一些開源的 go 項目經常會擺出一些與同類工具的性能基準測試對比,就是使用 go testing benchmark 輕鬆做出來的。

性能基準測試的代碼命名規範與普通 testing 代碼的要求類似,除部分性能基準測試專有的函數外,其它像報告打印、跳出機制都一樣。不過由於 benchmark 程序會多次執行,除非有必要,在測試函數中儘量少用打印函數。

go 性能基準測試代碼的函數命名:

func BenchmarkXxx(*testing.T)

這些基準測試代碼可以與普通的單元測試代碼放在一起。通常使用 go test 不會進行基準測試,需要添加參數。

還是 youwu_test.go

// youwu_test.go
package main_test

import (
    "fmt"
    "math/rand"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

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

使用命令 go test -bench='Rand' -v .得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -bench='Rand' -v .
hello, i'"init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:19: TestHello
--- PASS: TestHello (0.00s)
goos: darwin
goarch: amd64
pkg: hellotest
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkRandInt
BenchmarkRandInt-4      70186930                16.21 ns/op
PASS
😀 結束所有測試前
ok      hellotest       1.476s

可見,普通的單元測試代碼還是會被執行。使用 -bench='Rand' 參數來指定 go testing 執行 BenchmarkRandInt 這個函數。終端報告中的

BenchmarkRandInt-4      70186930                16.21 ns/op

表示運行了 70186930 次,每次 16.21 納秒。更多支持請看官方文檔,或者命令 go help test 查看關於 benchmark 部分。【導讀】阿斯蒂芬

接觸過 java 的同時,junit 這個名稱並不會陌生,或者 python 的測試框架 unittest。可是有習慣寫測試程序的卻聊聊無幾。在 golang 中,關於 testing 的支持比較完善,對於一般的函數測試來說,用不到第三方工具。

官方說明, golang testing https://golang.org/pkg/testing/

要在 go 程序開發過程中使用單元測試程序,無須安裝其它工具,安裝了 golang 環境時已經包含,golang 的工具鏈還是比較完善的。

關於單元測試代碼、測試用例、自動化測試等不是有悟想討論的,不同規模的工程、不同進度的項目要求、不同的管理方法,會讓管理者作出不同的決定。本文僅說說如果在 golang 程序中集成可測試代碼。

要想在 golang 使用 go test 工具鏈來實現代碼單元測試功能,只需要遵守 go test 工具的規範。

本文並未涉及的 golang testing 的所有內容,只會講有悟在寫測試程序用到的功能,完整內容見上面的官方說明鏈接。

以下從如何使用的角度來介紹 go testing,並不是大而全的框架性講解。

按照有悟的慣例,先準備一下示例代碼工程:

~/Projects/go/examples
➜ mkdir hellotest && cd hellotest

~/Projects/go/examples/hellotest
➜ go mod init hellotest

~/Projects/go/examples/hellotest
➜ touch youwutoday.go youwu_test.go
// youwutoday.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello testing...😀")
}
// youwu_test.go
package main_test

組織你的單元測試代碼

爲了更好的管理單元測試代碼,按照 go testing 的命名規範,運行 go testing 會掃描並編譯運行工程目錄下 xxx_test.go 代碼文件中的 TestXXX 測試函數。對一個沒有任何測試代碼的 go 程序工程運行 go testing 只會提示 no tests to run 並不會報錯。

不用擔心這些測試代碼管理會讓你的代碼工程變得混亂,你可以使用一個專門的工程級目錄(比如叫 test) 來存放所有主要的測試代碼。也可以在對應的包(比如 app)下使用對應測試包(比如 app_test)。這樣就可以利用人爲區分,用何種目錄方式完全取決於你的需要。但這對於 go 工具鏈來說不是必要的,因爲它並不會把這些測試代碼打包到程序的發行版本中。

關於 go testing 啓動命令

在上面的例子中,單元測試用例的啓動都是使用 go test 來啓動的,關於這個命令,有必要說明的地方。

go test .

僅從當前目錄查找 xxx_test.go 測試代碼

go test ./...

找出當前的目錄以及子目錄下所的 xxx_test.go 測試代碼

go test ./xxx/xxx

找出路徑下的 xxx_test.go 測試代碼,如果包含子目錄,使用 ./xxx/xxx/...,即在路徑後加上 /...,注意,是 3 個 “.” 號。

go test 路徑 -v

-v 參數,打開終端信息打印的開關。在沒有 -v 選項時,測試代碼中的打印函數(t.Logt.Skipt.Errort.Fatal)向終端打印的信息,只有測試函數狀態爲 FAIL 的纔會顯示。而 fmt.Print 只會在任一測試函數爲 FAIL 時,信息纔會顯示。所以在調試單元測試代碼時,通常會使用 -v 參數,可多次成功執行的,不帶 -v,以保持終端信息提示的清晰。這只是實踐建議,是否使用取決於你。

go test -run ^TestXXXX$

-run 用來指定執行某個 TestXXX 單元測試函數,支持使用正則表達式來匹配測試函數名稱。當然,像 TestMaininit() 這類框架性代碼仍然會被執行,以確保環境正確。

在測試中報錯與跳出

go testing 中,你可能見不到 assertgo testing 框架簡化了這一切的工作。在上面的例子,有悟使用了 t.Log 來打印終端信息。你可以使用 t.Error 來結合 if 判斷來打印信息,並標記這個測試出現錯誤。

// youwu_test.go
func TestReportError(t *testing.T) {
    t.Error("report in: t.Error")
}

會得到類似於下面這樣的錯誤報告:

...
=== RUN   TestReportError
    youwu_test.go:42: report in: t.Error
--- FAIL: TestReportError (0.00s)
...
exit status 1
FAIL    hellotest       0.498s

其實 t.Error 等同於 t.Log + t.Fail,當只標記錯誤不打印信息時,使用t.Fail

跳出單元測試函數的選擇:

t.SkipNow

跳出當前單元測試函數,狀態爲跳出(SKIP,不計爲失敗),執行下一個測試函數

t.Skip(信息)

t.Log -> t.SkipNow 打印並跳出當前單元測試函數

t.Fail

標記當前單元測試函數狀態爲錯誤(FAILED),繼續執行

t.FailNow

標記當前單元測試函數狀態爲錯誤(FAILED)並跳出,執行下一個測試函數

關於報錯機制,有如下幾種:

t.Error(信息)

t.Log(信息) -> t.Fail(),該單元測試會繼續執行

t.Fatal(信息)

t.Log(信息) -> t.FailNow(),跳出該單元測試,執行下一個單元測試函數

以上關於終端打印的函數 t.Logt.Errort.Fatal 都有對應的格式化形式 t.Logft.Errorft.Fatalf,它們的關係就像 fmt.Printfmt.Printf 的關係一樣。

測試程序初始化或清理

類似於 java junit 或者 python unittest,單元測試程序的運行分爲 SetupRuntearDown三個步驟,其中 setup 是用爲 Run 提供環境初始化的過程,比如連接數據庫、數據準備等等,tearDown 則是 Run 之後清理,比如刪除臨時文件、數據庫關閉等操作。

go testing 測試框架中並沒命名 SetuptearDown 函數。當然,你可以利用 go 程序本身的機制或者 go testing 提供的手段。

當然,如果你的單元測試代碼僅僅是一個無須外部數據支持的計算函數,那你根本無須理會初始化或者清理,直接將單元測試代碼編寫爲 xxx_test.goTestXXX 函數即可。

任何 go 代碼程序運行時,當前包、當前代碼文件中的 init() 函數會被最先執行。xxx_test.go 測試代碼的也不例外,那麼可以將初始化代碼放在這個函數內部。

(一個包內有多個初始化函數時,會按一定順序執行。將在另一篇文件中介紹 )

youwu_test.go 中添加

package main_test

import (
    "fmt"
    "testing"
)
func TestWorld(t *testing.T) {
    t.Log("TestWorlds")
}
func init() {
    fmt.Println("hello, i'm \"init\".")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
=== RUN   TestHello
    youwu_test.go:9: TestHello
--- PASS: TestHello (0.00s)
PASS
ok      hellotest       0.316s

測試函數級別:使用 t 實例的 Cleanup 方法註冊一個在單元測試程序退出前執行的函數。包級別:使用 TestMain 模式。

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
=== RUN   TestClean
    youwu_test.go:21: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
...

TestMaingo testing 測試框架的指定函數。用於控制整個測試過程,這個函數是包級別的。即一個包下,如果有多個 xxx_test.go 測試代碼,只能在其中某個 xxx_test.go 中定義。其基本格式大致如下:

在測試代碼中添加:

// youwu_test.go
package main_test

import (
    "fmt"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

// func TestWorld(t *testing.T) {
//  t.Log("TestWorlds")
// }

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

使用 go test -v 運行得到:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'"init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:10: TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestClean
    youwu_test.go:22: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
PASS
😀 結束所有測試前
ok      hellotest       0.369s

以上的順序,可以概括爲如下的流程:

`init()` → `TestMain` → TestXXX
                           ↓  
              ↑       ← t.Cleanup

benckmark | 性能基準測試

go testing 除了函數正確性測試之外,還支持性能基準測試(benchmark),即可多次運行,並計算其平均執行時間從而評估其運行效率。網上一些開源的 go 項目經常會擺出一些與同類工具的性能基準測試對比,就是使用 go testing benchmark 輕鬆做出來的。

性能基準測試的代碼命名規範與普通 testing 代碼的要求類似,除部分性能基準測試專有的函數外,其它像報告打印、跳出機制都一樣。不過由於 benchmark 程序會多次執行,除非有必要,在測試函數中儘量少用打印函數。

go 性能基準測試代碼的函數命名:

func BenchmarkXxx(*testing.T)

這些基準測試代碼可以與普通的單元測試代碼放在一起。通常使用 go test 不會進行基準測試,需要添加參數。

還是 youwu_test.go

// youwu_test.go
package main_test

import (
    "fmt"
    "math/rand"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

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

使用命令 go test -bench='Rand' -v .得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -bench='Rand' -v .
hello, i'"init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:19: TestHello
--- PASS: TestHello (0.00s)
goos: darwin
goarch: amd64
pkg: hellotest
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkRandInt
BenchmarkRandInt-4      70186930                16.21 ns/op
PASS
😀 結束所有測試前
ok      hellotest       1.476s

可見,普通的單元測試代碼還是會被執行。使用 -bench='Rand' 參數來指定 go testing 執行 BenchmarkRandInt 這個函數。終端報告中的

BenchmarkRandInt-4      70186930                16.21 ns/op

表示運行了 70186930 次,每次 16.21 納秒。更多支持請看官方文檔,或者命令 go help test 查看關於 benchmark 部分。

轉自:

youwu.today/skill/backend/how-to-test-your-go-code

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/24P5VdZNt5Z5mTWu7GmXPA