詳解Golang中單元測試的使用
介紹
單元測試有什么用?
單元測試是檢測你寫的一個(gè)函數(shù)是否具備安全性的一次檢測。比如,當(dāng)你寫了一個(gè)函數(shù),傳入你期望的值,該函數(shù)是能正常執(zhí)行的。但是當(dāng)你傳入一個(gè)非合理的值后,你寫的函數(shù)直接宕機(jī),而不是返回err。那就說明你寫的函數(shù)存在邏輯漏洞。一個(gè)通過單元測試的函數(shù),無論傳入什么類型的值,返回的要么是結(jié)果,要么是error,絕對不會(huì)停止運(yùn)行。
go test工具
在golang語言中存在測試依賴go test命令,go test命令會(huì)對包路徑下的所有_test.go為后綴的源代碼文件進(jìn)行測試,不會(huì)被go build編譯到可執(zhí)行文件中。
在*_test.go文件中,包含三種類型的函數(shù):單元測試函數(shù)、基準(zhǔn)測試函數(shù)、示例函數(shù)
類型 | 文件名 | 作用 |
---|---|---|
測試函數(shù) | TestXxxx | 測試程序的一些邏輯行為是否正確 |
基準(zhǔn)函數(shù) | BenchchmarkXxxx | 測試函數(shù)的性能 |
示例函數(shù) | ExampleXxxx | 為文檔提供示例文檔 |
go test 命令會(huì)對遍歷*_test.go文件中的符合上述規(guī)范的函數(shù),生成一個(gè)臨時(shí)的main包用于調(diào)用相應(yīng)的測試函數(shù),重構(gòu)、運(yùn)行、報(bào)告、測試,最后清除測試中的生成的臨時(shí)文件。
測試函數(shù)
每個(gè)測試函數(shù)都必須導(dǎo)入testing包。
// 用于對str字符串的子串截取 func subString(str string, start, end int) string { //將str轉(zhuǎn)換為[]rune,支持Unicode編碼 rs := []rune(str) length := len(rs) if start < 0 || start > length { panic("start is wrong") } if end < start || end > length { panic("end is wrong") } return string(rs[start:end]) }
上述函數(shù)傳入str、start、end參數(shù),分別表示原始字符串,起始下標(biāo),結(jié)束下標(biāo)。目的是為了獲取str的子串。
這里有個(gè)知識(shí)點(diǎn),是關(guān)于go語言的編碼的:
在golang中,其采納的編碼方式是UTF8的支持中文編碼方式,而非Unicode。在UTF8中,一個(gè)漢字是用三個(gè)字節(jié)來表示的。舉個(gè)列子,str := "hang行",其len(str)==7,如果你從start ==5 ,end == 6切割就會(huì)出現(xiàn)亂碼,相當(dāng)于你截取了漢字的三分之一。當(dāng)str轉(zhuǎn)換為[]rune類型后,len([]rune(str)) == 5,這個(gè)時(shí)候漢字就是一個(gè)整體,就能完整的截取"行"這個(gè)漢字。
func TestCode(t *testing.T) { // []rune與Unicode str := "i am your 爸爸" fmt.Printf("str的原始長度:%d\n", len(str)) fmt.Printf("str的[]rune類型長度:%d\n", len([]rune(str))) }
打印信息:
API server listening at: 127.0.0.1:64049
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN TestCode
str的原始長度:16
str的[]rune類型長度:12
--- PASS: TestCode (0.00s)
PASS
對[]rune的測試完畢,回到測試函數(shù)中:
func TestSubString(t *testing.T) { got := subString("hang", 0, 1) want := "h" if !reflect.DeepEqual(got, want) { t.Error("subString test is failed") } }
打印信息:
API server listening at: 127.0.0.1:57824
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN TestSubString
--- PASS: TestSubString (0.00s)
PASS
在該測試函數(shù)中,got為函數(shù)返回信息;want為期望值。經(jīng)過reflect.DeepEqual(got,want)函數(shù)進(jìn)行匹配比較。
reflect.DeepEqual(got,want)函數(shù):用于比較兩個(gè)值的深度相等性。它可以用來在測試中判斷期望的值和實(shí)際得到的值是否相等。reflect.DeepEqual
對比的是值的具體內(nèi)容,而不僅僅是內(nèi)存地址或類型。用于確保值的類型和結(jié)構(gòu)是一致的。
testing.T類型的方法 | 作用 |
---|---|
Error | 輸出一個(gè)錯(cuò)誤消息,并標(biāo)記該測試為失敗。 |
Errorf | 格式化輸出一個(gè)錯(cuò)誤消息,并標(biāo)記該測試為失敗。 |
Fail | 標(biāo)記該測試為失敗。 |
FailNow | 標(biāo)記該測試為失敗,并立即停止當(dāng)前測試函數(shù)的執(zhí)行。 |
Fatal | 輸出一個(gè)致命錯(cuò)誤消息,并終止測試執(zhí)行 |
Fatalf | 格式化輸出一個(gè)致命錯(cuò)誤消息,并終止測試執(zhí)行。 |
Name | 返回當(dāng)前測試的名稱。 |
Run | 運(yùn)行一個(gè)子測試函數(shù),并指定其名稱。 |
目前常用的testing.T類型的打印信息函數(shù)是上訴這幾個(gè)。如果對其他函數(shù)感興趣,可以查閱資料。
測試函數(shù)是能夠執(zhí)行了,但是只能針對一個(gè)測試用例。如果有多個(gè)測試用例呢?這就引入了子測試函數(shù)的使用:
func TestSubString(t *testing.T) { type test struct { str string start int end int want interface{} } es := map[string]test{ "basic": {str: "basic", start: 0, end: 4, want: "basi"}, //basi "basicCN": {str: "你好world", start: 0, end: 1, want: "你"}, //"你" } for name, testValue := range es { t.Run(name, func(t *testing.T) { got := subString(testValue.str, testValue.start, testValue.end) if !reflect.DeepEqual(got, testValue.want) { t.Errorf("the name== %s got %v, want %v", t.Name(), got, testValue.want) } }) } }
打印信息:
API server listening at: 127.0.0.1:64621
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN TestSubString
=== RUN TestSubString/basic
=== RUN TestSubString/basicCN
--- PASS: TestSubString (0.00s)
--- PASS: TestSubString/basic (0.00s)
--- PASS: TestSubString/basicCN (0.00s)
PASS
說明:在我寫的函數(shù)SubString中,有 panic("start is wrong") 語句,這個(gè)結(jié)果在測試單元中是無法want的,即這種結(jié)果是無法測試。但是這個(gè)函數(shù)對我的項(xiàng)目是很重要的,未達(dá)到start和end就必須要panic。無需去深究,你可以將panic換成error.new("XXXX")。測試函數(shù)的部分知識(shí)就講解完畢了,文章后續(xù)有案例實(shí)戰(zhàn)。
基準(zhǔn)測試
基準(zhǔn)測試是在一定的工作負(fù)載下檢測程序性能的一種方法。函數(shù)一定是先通過測試函數(shù)之后,才開始對該函數(shù)性能的檢測。很多程序員在開發(fā)了一套函數(shù)時(shí),就會(huì)有一種心態(tài):能跑就行。其實(shí)這樣想也沒問題,工作嘛。基準(zhǔn)測試也是檢測自身編寫代碼的性能。很簡單一個(gè)道理,同樣是寫一樣功能的代碼,為什么就用其他的,而不用你的,因?yàn)槠渌脑谛阅軠y試上超越了你。
基準(zhǔn)測試是以Benchmark為開頭的,需要以*testing.B類型的參數(shù)。
func BenchmarkSubstring(b *testing.B) { for i := 0; i < b.N; i++ { subString("Benchmark", 0, 5) } }
打印信息:
goos: windows
goarch: amd64
pkg: sspaas.io/gpu-manage/utils/data
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkSubstring
BenchmarkSubstring-8 14777178 81.77 ns/op
PASS
相較于單元測試,基準(zhǔn)測試使用得不是很多。
BenchmarkSubstring-8 14777178 81.77 ns/op
表示Substring函數(shù)在迭代14777178次下,平均每次消耗81.77 納秒。b.N默認(rèn)是很大的一個(gè)數(shù),你也可以自定義迭代次數(shù)。一般是不要去動(dòng)b.N
基準(zhǔn)測試還有很多知識(shí)點(diǎn),包括內(nèi)存消耗的檢測、測試代碼的覆蓋率等。一般來說,作為開發(fā)人員僅僅測試一下迭代時(shí)間即可。若想了解更多,自行查閱資料。
示例函數(shù)
在單元測試中,示例函數(shù)(Example Functions)是一種特殊的函數(shù),用于提供對被測試函數(shù)的示例使用方法、參數(shù)和期望輸出的演示。
func ExampleFunction() { result := YourFunction(input) fmt.Println(result) // Output: expected_output }
閑的沒事的人,才去寫示例函數(shù)。有這時(shí)間,還不如去泡杯茶····
實(shí)戰(zhàn)
對aes加密解密的aes.go文件進(jìn)行單元測試
import ( "bytes" "crypto/aes" "crypto/cipher" "encoding/base64" ) // AesEncrypt AES加密 func AesEncrypt(orig string, key string) string { // 轉(zhuǎn)換成字節(jié)數(shù)組 origData := []byte(orig) k := []byte(key) // 分組秘鑰 block, _ := aes.NewCipher(k) // 獲取秘鑰塊的長度 blockSize := block.BlockSize() // 補(bǔ)全碼 origData = PKCS7Padding(origData, blockSize) // 加密模式 blockMode := cipher.NewCBCEncrypter(block, k[:blockSize]) // 創(chuàng)建數(shù)組 cryted := make([]byte, len(origData)) // 加密 blockMode.CryptBlocks(cryted, origData) return base64.StdEncoding.EncodeToString(cryted) } // AesDecrypt AES解密 func AesDecrypt(cryted string, key string) string { // 轉(zhuǎn)成字節(jié)數(shù)組 crytedByte, _ := base64.StdEncoding.DecodeString(cryted) k := []byte(key) // 分組秘鑰 block, _ := aes.NewCipher(k) // 獲取秘鑰塊的長度 blockSize := block.BlockSize() // 加密模式 blockMode := cipher.NewCBCDecrypter(block, k[:blockSize]) // 創(chuàng)建數(shù)組 orig := make([]byte, len(crytedByte)) // 解密 blockMode.CryptBlocks(orig, crytedByte) // 去補(bǔ)全碼 orig = PKCS7UnPadding(orig) return string(orig) } // PKCS7Padding 補(bǔ)碼 func PKCS7Padding(ciphertext []byte, blocksize int) []byte { padding := blocksize - len(ciphertext)%blocksize padtext := bytes.Repeat([]byte{byte(padding)}, padding) return append(ciphertext, padtext...) } // PKCS7UnPadding 去碼 func PKCS7UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] }
import ( "reflect" "testing" ) /*----------------------單元測試----------------------------------------*/ const key = "q+#@,6%ej-QP^mbc" func TestAesEncrypt(t *testing.T) { type test struct { org string key string want string } es := map[string]test{ "passed": {org: "basic", key: key, want: "2+1s9cqk1NzIUnq+G3VgEg=="}, //passed } for _, value := range es { got := AesEncrypt(value.org, value.key) if !reflect.DeepEqual(got, value.want) { t.Errorf("the name== %s got %v, want %v", t.Name(), got, value.want) } } } /* API server listening at: 127.0.0.1:54671 WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18) === RUN TestAesEncrypt --- PASS: TestAesEncrypt (0.00s) PASS */ func BenchmarkAesEncrypt(b *testing.B) { for i := 0; i < b.N; i++ { AesEncrypt("test", key) } } /*pkg: sspaas.io/gpu-manage/utils/aes cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz BenchmarkAesEncrypt BenchmarkAesEncrypt-8 1000000 1031 ns/op PASS */ func TestAesDecrypt(t *testing.T) { type test struct { org string key string want string } es := map[string]test{ "passed": {org: "2+1s9cqk1NzIUnq+G3VgEg==", key: key, want: "basic"}, //passed } for _, value := range es { got := AesDecrypt(value.org, value.key) if !reflect.DeepEqual(got, value.want) { t.Errorf("the name== %s got %v, want %v", t.Name(), got, value.want) } } } /*API server listening at: 127.0.0.1:54693 WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18) === RUN TestAesDecrypt --- PASS: TestAesDecrypt (0.00s) PASS */ func BenchmarkAesDecrypt(b *testing.B) { for i := 0; i < b.N; i++ { AesDecrypt("2+1s9cqk1NzIUnq+G3VgEg==", key) } } /*pkg: sspaas.io/gpu-manage/utils/aes cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz BenchmarkAesDecrypt BenchmarkAesDecrypt-8 1334907 883.0 ns/op PASS */
到此這篇關(guān)于詳解Golang中單元測試的使用的文章就介紹到這了,更多相關(guān)Golang單元測試內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go定時(shí)器的三種實(shí)現(xiàn)方式示例詳解
這篇文章主要為大家介紹了Go定時(shí)器的三種實(shí)現(xiàn)方式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12go?micro微服務(wù)proto開發(fā)安裝及使用規(guī)則
這篇文章主要為大家介紹了go?micro微服務(wù)proto開發(fā)中安裝Protobuf及基本規(guī)范字段的規(guī)則詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01golang json.Marshal 特殊html字符被轉(zhuǎn)義的解決方法
今天小編就為大家分享一篇golang json.Marshal 特殊html字符被轉(zhuǎn)義的解決方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08最新版Golang?pprof使用詳解(引入、抓取、分析,圖文結(jié)合)
這篇文章主要介紹了最新版Golang?pprof使用詳解包括引入、抓取、分析,圖文結(jié)合,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-08-08Prometheus Go client library使用方式詳解
這篇文章主要為大家介紹了Prometheus Go client library使用方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11go語言實(shí)現(xiàn)同步操作項(xiàng)目示例
本文主要介紹了go語言實(shí)現(xiàn)同步操作項(xiàng)目示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05