Go?語言進(jìn)階單元測試示例詳解
前言
本文從單元測試實(shí)踐角度出發(fā),提升對(duì)代碼質(zhì)量的意識(shí)。
本文內(nèi)容主要包括:單元測試、Mock測試、基準(zhǔn)測試。
測試
測試可以提高代碼的質(zhì)量、減少事故的發(fā)生。
測試又分為:回歸測試、集成測試、單元測試。
回歸測試是指對(duì)QA手動(dòng)回歸一些特定場景,可以理解為我們說的手動(dòng)點(diǎn)點(diǎn)。
集成測試是指對(duì)系統(tǒng)功能維度做驗(yàn)證,比如對(duì)服務(wù)暴露的接口驗(yàn)證,一般是自動(dòng)化的驗(yàn)證。
單元測試是指在開發(fā)階段,開發(fā)者對(duì)單獨(dú)的函數(shù)、模塊做驗(yàn)證,寫一些測試用例。

單元測試
單元測試組成部分:輸入、輸出、測試單元、與期望的校對(duì),測試單元又包括函數(shù)、接口、模塊、復(fù)雜的聚合函數(shù)等。
通過單元測試的輸出再與期望輸出進(jìn)行校對(duì),來驗(yàn)證代碼的正確性。通過單元測試可以保證代碼的質(zhì)量,也可以在一定程度上提升效率,比如通過運(yùn)行單元測試可以快速定位到有問題的代碼。

規(guī)則
單元測試的編寫有一定的規(guī)則:
- 所有測試文件以
_test.go結(jié)尾 - 測試方法名以
Test開頭,參數(shù)要用testingfunc TestXxx(t *testing.T) - 測試初始化邏輯放到
TestMain中 - 通過
go test命令進(jìn)行測試
示例
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutPut := "Tom"
if output != expectOutPut {
t.Errorf("Expected %s do not match actual %s", expectOutPut, output)
}
}
func HelloTom() string {
return "Jerry"
}
通過go test命令運(yùn)行得到以下結(jié)果,從測試結(jié)果里可以看出,我們期望得到的是Tom,但實(shí)際得到的卻是Jerry。
--- FAIL: TestHelloTom (0.00s) helloTom_test.go:9: Expected Tom do not match actual Jerry FAIL exit status 1 FAIL learning/mytesting 0.496s
assert
另外我們可以使用開源的assert包,來代替我們自己的if判斷。
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutPut := "Tom"
assert.Equal(t, expectOutPut, output)
}
再次通過go test命令運(yùn)行得到以下結(jié)果,輸出了更詳細(xì)的堆棧信息。

覆蓋率
如何評(píng)估單元測試呢?是通過代碼覆蓋率來評(píng)估的,評(píng)估的標(biāo)準(zhǔn)包括:
- 衡量代碼是否經(jīng)過了足夠的測試
- 評(píng)價(jià)項(xiàng)目的測試水準(zhǔn)
- 評(píng)估項(xiàng)目是否達(dá)到了高水平的測試等級(jí)
下面通過一個(gè)例子來看下代碼覆蓋率:
// judgepass.go
func JudgePassLine(score int16) bool {
if score >= 16 {
return true
}
return false
}
// judgepass_test.go
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
通過命令來看覆蓋率go test judgepass_test.go judgepass.go --cover
輸出結(jié)果為:
ok command-line-arguments 0.327s coverage: 66.7% of statements
可以看到,提示出的代碼覆蓋率為66.7%,因?yàn)橹蛔吡艘粋€(gè)if分支。如果要想達(dá)到100%的代碼覆蓋率的話,就要把所有的分支都要覆蓋到。
一般的覆蓋率為50%~60%,較高的覆蓋率要達(dá)到80%+。要注意:測試分支相互獨(dú)立、要全面覆蓋,測試單元粒度要足夠小,滿足函數(shù)單一職責(zé)。
依賴
一個(gè)實(shí)際項(xiàng)目不可能只是一個(gè)簡單的單體函數(shù),肯定會(huì)很復(fù)雜,存在其他的依賴,比如依賴數(shù)據(jù)庫、redis、文件等外部依賴。
單元測試一般有兩個(gè)目標(biāo):冪等、穩(wěn)定。
冪等:重復(fù)執(zhí)行一個(gè)用例、調(diào)用一個(gè)接口,返回的結(jié)果是一樣的。
穩(wěn)定:單元測試是相互隔離的,在任何時(shí)間都能獨(dú)立運(yùn)行。

Mock
如果單元測試用到數(shù)據(jù)庫、redis等,在單元測試?yán)镏苯舆B接會(huì)涉及到網(wǎng)絡(luò)傳輸,這是不穩(wěn)定的,所以要用到Mock機(jī)制。
開源Mock框架:github.com/bouk/monkey
這個(gè)Mock包可以對(duì)函數(shù)或方法進(jìn)行打樁,打樁就是用一個(gè)函數(shù)A來替換一個(gè)函數(shù)B。
monkey的實(shí)現(xiàn)原理主要是在運(yùn)行時(shí),通過Go的unsafe包能夠?qū)?nèi)存中函數(shù)的地址替換為運(yùn)行時(shí)函數(shù)的地址,最終調(diào)用的是打樁函數(shù),從而實(shí)現(xiàn)Mock的功能。
Mock常用方法:Patch、Unpatch。
Patch方法有兩個(gè)參數(shù),target為替換的函數(shù)(原函數(shù)),replacement為要替換成的函數(shù)。
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
Unpatch為測試結(jié)束之后,要把打的樁給卸載掉。
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
下面通過Mock來模擬對(duì)文件的操作。
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
該測試用例對(duì)ProcessFirstLine函數(shù)進(jìn)行測試,這個(gè)函數(shù)調(diào)用了ReadFirstLine函數(shù),涉及到文件的操作,通過Mock對(duì)文件的操作進(jìn)行打樁,這樣就避免了其他進(jìn)程對(duì)文件操作的影響。
基準(zhǔn)測試
Go還提供了基準(zhǔn)測試框架,可以測試一段程序的性能、CPU消耗,可以對(duì)代碼做性能分析,測試方法與單元測試類似。
基準(zhǔn)測試規(guī)則:
- 基準(zhǔn)測試以Benchmark為前綴
- 需要一個(gè)*testing.B類型的參數(shù)b
- 基準(zhǔn)測試必須要執(zhí)行b.N次
下面通過一個(gè)模擬負(fù)載均衡的例子,來看下基準(zhǔn)測試:
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func (pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
通過命令 go test -bench=. 運(yùn)行測試,輸出結(jié)果如下:
goos: darwin
goarch: amd64
pkg: learning/bench
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.70GHz
BenchmarkSelect-8 50264580 23.47 ns/op
BenchmarkSelectParallel-8 13717840 133.4 ns/op
PASS
ok learning/bench 4.559s
BenchmarkSelect-8 表示對(duì)Select函數(shù)進(jìn)行基準(zhǔn)測試,數(shù)字8表示 GOMAXPROCS 的值。
23.47 ns/op 表示每次調(diào)用Select函數(shù)耗時(shí)23.47ns。
50264580 這是50264580次調(diào)用的平均值。
字節(jié)開源的go框架:github.com/bytedance/g…
以上就是Go 語言進(jìn)階單元測試示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Go 語言單元測試的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言同步等待組sync.WaitGroup結(jié)構(gòu)體對(duì)象方法詳解
這篇文章主要為大家介紹了Go語言同步等待組sync.WaitGroup結(jié)構(gòu)體對(duì)象方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Golang?channel底層實(shí)現(xiàn)過程解析(深度好文)
Go語言為了方便使用者,提供了簡單、安全的協(xié)程數(shù)據(jù)同步和通信機(jī)制,這篇文章主要介紹了Golang?channel底層是如何實(shí)現(xiàn)的,需要的朋友可以參考下2024-07-07
基于Go+WebSocket實(shí)現(xiàn)實(shí)時(shí)通信功能
在互聯(lián)網(wǎng)應(yīng)用程序中,實(shí)時(shí)通信是一種非常重要的功能,WebSocket 是一種基于 TCP 的協(xié)議,它允許客戶端和服務(wù)器之間進(jìn)行雙向通信,本文將介紹如何使用 Golang 創(chuàng)建單獨(dú)的 WebSocket 會(huì)話,以實(shí)現(xiàn)實(shí)時(shí)通信功能,需要的朋友可以參考下2023-10-10
Golang使用sqlite3數(shù)據(jù)庫實(shí)現(xiàn)CURD操作
這篇文章主要為大家詳細(xì)介紹了Golang使用sqlite3數(shù)據(jù)庫實(shí)現(xiàn)CURD操作的相關(guān)知識(shí),文中的示例代碼簡潔易懂,有需要的小伙伴可以參考一下2025-03-03

