亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

詳解如何在Go中如何編寫出可測(cè)試的代碼

 更新時(shí)間:2023年08月21日 09:33:26   作者:江湖十年  
在編寫測(cè)試代碼之前,還有一個(gè)很重要的點(diǎn),容易被忽略,就是什么樣的代碼是可測(cè)試的代碼,所以本文就來聊一聊在?Go?中如何寫出可測(cè)試的代碼吧

之前寫了幾篇文章,介紹在 Go 中如何編寫測(cè)試代碼,以及如何解決被測(cè)試代碼中的外部依賴問題。但其實(shí)在編寫測(cè)試代碼之前,還有一個(gè)很重要的點(diǎn),容易被忽略,就是什么樣的代碼是可測(cè)試的代碼?為了更方便的編寫測(cè)試,我們?cè)诰幋a階段就應(yīng)該要考慮到,自己寫出來的代碼是否能夠被測(cè)試。本文就來聊一聊在 Go 中如何寫出可測(cè)試的代碼。

本文不講理論,只講我在實(shí)際開發(fā)過程中的經(jīng)驗(yàn)和思考,使用幾個(gè)實(shí)際的案例,來演示怎樣從根上解決測(cè)試代碼難以編寫的問題。

使用變量來定義函數(shù)

假設(shè)我們編寫了一個(gè) Login 函數(shù),用來實(shí)現(xiàn)用戶登錄,示例代碼如下:

func Login(u User) (string, error) {
	// ...
	token, err := GenerateToken(32)
	if err != nil {
		// ...
	}
	// ...
	return token, nil
}

Login 函數(shù)接收 User 信息,并在內(nèi)部通過 GenerateToken(32) 函數(shù)生成一個(gè) 32 位長(zhǎng)度的隨機(jī) token 作為認(rèn)證信息,最終返回 token。

這個(gè)函數(shù)只編寫了大體框架,具體細(xì)節(jié)沒有實(shí)現(xiàn),但我們可以發(fā)現(xiàn),Login 函數(shù)內(nèi)部依賴了 GenerateToken 函數(shù)。

GenerateToken 函數(shù)定義如下:

func GenerateToken(n int) (string, error) {
	token := make([]byte, n)
	_, err := rand.Read(token)
	if err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(token)[:n], nil
}

現(xiàn)在我們要為 Login 函數(shù)編寫單元測(cè)試,可以寫出如下測(cè)試代碼:

func TestLogin(t *testing.T) {
	u := User{
		ID:     1,
		Name:   "test1",
		Mobile: "13800001111",
	}
	token, err := Login(u)
	assert.NoError(t, err)
	assert.Equal(t, 32, len(token))
}

可以發(fā)現(xiàn),在調(diào)用 Login 函數(shù)后,我們只能斷言獲得的 token 長(zhǎng)度,而無法斷言 token 具體內(nèi)容,因?yàn)?GenerateToken 函數(shù)每次隨機(jī)生成的 token 值是不一樣的。

這看起來似乎沒什么問題,但通常情況下,我們應(yīng)該盡量避免測(cè)試代碼中出現(xiàn)隨機(jī)性的值。并且,有可能被測(cè)試代碼較為復(fù)雜,比如我們要測(cè)試的是調(diào)用 Login 函數(shù)的上層函數(shù),那么這個(gè)函數(shù)可能還會(huì)使用 token 去做其他的事情。此時(shí),就會(huì)出現(xiàn)代碼無法被測(cè)試的情況。

所以,在編寫測(cè)試時(shí),我們應(yīng)該讓 GenerateToken 函數(shù)的返回結(jié)果固定下來,但現(xiàn)在定義的 GenerateToken 函數(shù)顯然無法做到這一點(diǎn)。

要解決這個(gè)問題,我們需要重新定義下 GenerateToken 函數(shù):

var GenerateToken = func(n int) (string, error) {
	token := make([]byte, n)
	_, err := rand.Read(token)
	if err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(token)[:n], nil
}

GenerateToken 函數(shù)內(nèi)部邏輯沒變,不過換了一種定義方式。GenerateToken 不再是函數(shù)名,而是一個(gè)變量名,這個(gè)變量指向了一個(gè)匿名函數(shù)。

現(xiàn)在我們就有機(jī)會(huì)在測(cè)試 Login 的時(shí)候,將 GenerateToken 變量進(jìn)行替換,實(shí)現(xiàn)一個(gè)只會(huì)返回固定輸出的 GenerateToken 函數(shù)。

新版單元測(cè)試代碼實(shí)現(xiàn)如下:

func TestLogin(t *testing.T) {
	u := User{
		ID:     1,
		Name:   "test1",
		Mobile: "13800001111",
	}
	token, err := Login(u)
	assert.NoError(t, err)
	assert.Equal(t, 32, len(token))
	assert.Equal(t, "jCnuqKnsN5UAM9-LgEGS_COvJWp15RDv", token)
}
func init() {
	GenerateToken = func(n int) (string, error) {
		return "jCnuqKnsN5UAM9-LgEGS_COvJWp15RDv", nil
	}
}

我們利用 init 函數(shù),在測(cè)試文件執(zhí)行一開始就替換了 GenerateToken 變量的指向,新的匿名函數(shù)返回固定的 token。這樣一來,在測(cè)試時(shí) Login 函數(shù)內(nèi)部調(diào)用的就是 GenerateToken 變量所指向的函數(shù)了,其返回值已經(jīng)被固定,因此,我們可以對(duì)其進(jìn)行斷言操作。

使用依賴注入來解決外部依賴

現(xiàn)在我們有一個(gè) GenerateJWT 函數(shù),用來生成 JSON Web Token,其實(shí)現(xiàn)如下:

func GenerateJWT(issuer string, userId string, expire time.Duration, privateKey *rsa.PrivateKey) (string, error) {
	nowSec := time.Now().Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.MapClaims{
		"expiresAt": nowSec + int64(expire.Seconds()),
		"issuedAt":  nowSec,
		"issuer":    issuer,
		"subject":   userId,
	})
	return token.SignedString(privateKey)
}

這個(gè)函數(shù)使用當(dāng)前時(shí)間戳作為 payload,并且使用了 RS512,來生成 JWT。

此時(shí),我們要為這個(gè)函數(shù)編寫一個(gè)單元測(cè)試,代碼如下:

func TestGenerateJWT(t *testing.T) {
	key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
	assert.NoError(t, err)

	token, err := GenerateJWT("jianghushinian", "1234", 2*time.Hour, key)
	assert.NoError(t, err)
	assert.Equal(t, 499, len(token))
}

因?yàn)?GenerateJWT 函數(shù)生成 token 所使用的 payload 是依賴當(dāng)前時(shí)間的(time.Now().Unix()),故每次生成的 token 都會(huì)不同。所以同之前的 GenerateToken 函數(shù)一樣,我們也無法斷言 GenerateJWT 返回的 token 內(nèi)容,只能斷言其長(zhǎng)度。

但這是不合理的,斷言 token 長(zhǎng)度僅能表示這個(gè) token 生成出來了,但是不保證正確。因?yàn)?JWT 有很多算法,假如在編寫 GenerateJWT 函數(shù)時(shí)選錯(cuò)了算法,比如選成了 RS256,那么 TestGenerateJWT 函數(shù)就無法測(cè)試出來這個(gè) BUG。

為了提高 GenerateJWT 函數(shù)的測(cè)試覆蓋率,我們需要解決 time.Now().Unix() 依賴問題。

這次我們不再采用變量 + init 函數(shù)的方式,而是采用依賴注入的思想,將外部依賴當(dāng)做函數(shù)的參數(shù)傳遞進(jìn)來:

func GenerateJWT(issuer string, userId string, nowFunc func() time.Time, expire time.Duration, privateKey *rsa.PrivateKey) (string, error) {
	nowSec := nowFunc().Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.MapClaims{
		"expiresAt": nowSec + int64(expire.Seconds()),
		"issuedAt":  nowSec,
		"issuer":    issuer,
		"subject":   userId,
	})
	return token.SignedString(privateKey)
}

可以發(fā)現(xiàn),所謂的依賴注入,就是當(dāng) GenerateJWT 函數(shù)依賴當(dāng)前時(shí)間時(shí),我們不再通過 GenerateJWT 函數(shù)內(nèi)部直接調(diào)用 time.Now() 來獲取,而是使用參數(shù)(nowFunc)的方式,將 time.Now 函數(shù)傳遞進(jìn)來,當(dāng)函數(shù)內(nèi)部需要獲取當(dāng)前時(shí)間時(shí),就調(diào)用傳遞進(jìn)來的函數(shù)參數(shù)。

這樣,我們便實(shí)現(xiàn)了將依賴移動(dòng)到函數(shù)外部,在調(diào)用函數(shù)時(shí),將依賴從外部注入到函數(shù)內(nèi)部來使用。

現(xiàn)在實(shí)現(xiàn)的單元測(cè)試代碼就可以斷言生成的 token 是否正確了:

func TestGenerateJWT(t *testing.T) {
	key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey))
	assert.NoError(t, err)
	nowFunc := func() time.Time {
		return time.Unix(1689815972, 0)
	}
	actual, err := GenerateJWT("jianghushinian", "1234", nowFunc, 2*time.Hour, key)
	assert.NoError(t, err)
	expected := "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVzQXQiOjE2ODk4MjMxNzIsImlzc3VlZEF0IjoxNjg5ODE1OTcyLCJpc3N1ZXIiOiJqaWFuZ2h1c2hpbmlhbiIsInN1YmplY3QiOiIxMjM0In0.NmCDxFaBfAPPgWQ0zVMl8ON1UQMeIVNgFCn1vtbppsunb-VrOMCdnJlguvPnNc6fMD9EkzMYM3Ux8zFnTiICDMRX23UlhAo2Zb3DorThdrBcNWHMUd26DBNI9n_oUY5B6NPqtrutvqCex9lQH0vUYOt2O5dOyZ-H9cVNY1r3fJHNkYuNWxmoZRfka5o1oSWvUw8hBJfgjANOzZ5ACIi0q5hnou5hQ8VljjFsP4zj2a2lU6w5Db8_rOA04BxilkfurdExcPeaAVCtA-Km0zNwL3gGwJB21gwyb4MRHsEf-ra-4-V7O5_JGiSOQgfkNB63RoASljRXpD6q-gakm0e0fA"
	assert.Equal(t, expected, actual)
}

在單元測(cè)試中,調(diào)用 GenerateJWT 函數(shù)時(shí),我們可以使用一個(gè)返回固定值的 nowFunc 函數(shù)來作為 time.Now 的替代品。這樣當(dāng)前時(shí)間就被固定下來,因而 GenerateJWT 函數(shù)的返回結(jié)果也就被固定下來,就可以斷言 GenerateJWT 函數(shù)生成的 token 是否正確了。

提示:expected 的值可以在這個(gè)網(wǎng)站 生成,測(cè)試所用到的 private.pempublic.pem 文件我都放在了這里

對(duì)于 GenerateJWT 函數(shù),我還編寫了一個(gè) JWT.GenerateToken 方法版本,代碼如下:

type JWT struct {
	privateKey *rsa.PrivateKey
	issuer     string
	// nowFunc is used to mock time in tests
	nowFunc func() time.Time
}
func NewJWT(issuer string, privateKey *rsa.PrivateKey) *JWT {
	return &JWT{
		privateKey: privateKey,
		issuer:     issuer,
		nowFunc:    time.Now,
	}
}
func (j *JWT) GenerateToken(userId string, expire time.Duration) (string, error) {
	nowSec := j.nowFunc().Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.MapClaims{
		// map 會(huì)對(duì)其進(jìn)行重新排序,排序結(jié)果影響簽名結(jié)果,簽名結(jié)果驗(yàn)證網(wǎng)址:https://jwt.io/
		"issuer":    j.issuer,
		"issuedAt":  nowSec,
		"expiresAt": nowSec + int64(expire.Seconds()),
		"subject":   userId,
	})
	return token.SignedString(j.privateKey)
}

對(duì)于 TestJWT_GenerateToken 單元測(cè)試函數(shù)的實(shí)現(xiàn),就交給你自己來完成了。

使用接口來解耦代碼

我們有一個(gè) GetChangeLog 函數(shù)可以返回項(xiàng)目的 ChangeLog,實(shí)現(xiàn)如下:

var version = "dev"
type ChangeLogSpec struct {
	Version   string
	ChangeLog string
}
func GetChangeLog(f *os.File) (ChangeLogSpec, error) {
	data, err := io.ReadAll(f)
	if err != nil {
		return ChangeLogSpec{}, err
	}
	return ChangeLogSpec{
		Version:   version,
		ChangeLog: string(data),
	}, nil
}

GetChangeLog 函數(shù)接收一個(gè)文件對(duì)象 *os.File,使用 io.ReadAll(f) 從文件對(duì)象中讀取全部的 ChangeLog 內(nèi)容并返回。

如果要測(cè)試這個(gè)函數(shù),我們需要在單元測(cè)試中創(chuàng)建一個(gè)臨時(shí)文件,測(cè)試完成后還要對(duì)臨時(shí)文件進(jìn)行清理,實(shí)現(xiàn)代碼如下:

func TestGetChangeLog(t *testing.T) {
	expected := ChangeLogSpec{
		Version: "v0.1.1",
		ChangeLog: `
# Changelog
All notable changes to this project will be documented in this file.
`,
	}
	f, err := os.CreateTemp("", "TEST_CHANGELOG")
	assert.NoError(t, err)
	defer func() {
		_ = f.Close()
		_ = os.RemoveAll(f.Name())
	}()
	data := `
# Changelog
All notable changes to this project will be documented in this file.
`
	_, err = f.WriteString(data)
	assert.NoError(t, err)
	_, _ = f.Seek(0, 0)
	actual, err := GetChangeLog(f)
	assert.NoError(t, err)
	assert.Equal(t, expected, actual)
}

在測(cè)試時(shí),為了構(gòu)造一個(gè) *os.File 對(duì)象,我們不得不創(chuàng)建一個(gè)真正的文件。好在 Go 提供了 os.CreateTemp 方法能夠在操作系統(tǒng)的臨時(shí)目錄創(chuàng)建文件,方便清理工作。

其實(shí),我們還有更好的方式來實(shí)現(xiàn)這個(gè) GetChangeLog 函數(shù):

func GetChangeLog(reader io.Reader) (ChangeLogSpec, error) {
	data, err := io.ReadAll(reader)
	if err != nil {
		return ChangeLogSpec{}, err
	}
	return ChangeLogSpec{
		Version:   version,
		ChangeLog: string(data),
	}, nil
}

我對(duì) GetChangeLog 函數(shù)進(jìn)行了小改造,函數(shù)參數(shù)不再是一個(gè)具體的文件對(duì)象,而是一個(gè) io.Reader 接口類型。

GetChangeLog 函數(shù)內(nèi)部代碼無需改變,函數(shù)和它的外部依賴,就已經(jīng)通過接口完成了解耦。

現(xiàn)在,測(cè)試過程中我們可以使用 Fake obejct 或者 Mock object 來替換真實(shí)的 *os.File 對(duì)象。

使用 Fake obejct 實(shí)現(xiàn)測(cè)試代碼如下:

type fakeReader struct {
	data   string
	offset int
}
func NewFakeReader(input string) io.Reader {
	return &fakeReader{
		data:   input,
		offset: 0,
	}
}
func (r *fakeReader) Read(p []byte) (int, error) {
	if r.offset >= len(r.data) {
		return 0, io.EOF // 表示數(shù)據(jù)已讀取完畢
	}
	n := copy(p, r.data[r.offset:]) // 將數(shù)據(jù)從字符串復(fù)制到 p 中
	r.offset += n
	return n, nil
}
func TestGetChangeLogByIOReader(t *testing.T) {
	expected := ChangeLogSpec{
		Version: "v0.1.1",
		ChangeLog: `
# Changelog
All notable changes to this project will be documented in this file.
`,
	}
	data := `
# Changelog
All notable changes to this project will be documented in this file.
`
	reader := NewFakeReader(data)
	actual, err := GetChangeLogByIOReader(reader)
	assert.NoError(t, err)
	assert.Equal(t, expected, actual)
}

這一次,我們沒有直接創(chuàng)建一個(gè)真實(shí)的文件對(duì)象,而是提供一個(gè)實(shí)現(xiàn)了 io.Reader 接口的 fakeReader 對(duì)象。

在測(cè)試時(shí),可以使用這個(gè) fakeReader 來替代文件對(duì)象,而不必在操作系統(tǒng)中創(chuàng)建文件。

此外,因?yàn)槭褂昧私涌趤斫怦?,我們還可以使用 Mock 技術(shù)來編寫測(cè)試代碼。

不過 io.Reader 是一個(gè) Go 語言內(nèi)置接口,gomock 無法直接為其生成 Mock 代碼。

解決辦法是,我們可以為其起一個(gè)別名:

type IReader io.Reader

然后再為 IReader 接口實(shí)現(xiàn) Mock 代碼。

還可以對(duì) io.Reader 進(jìn)行一層包裝:

type ReaderWrapper interface {
	io.Reader
}

然后再為 ReaderWrapper 接口實(shí)現(xiàn) Mock 代碼。

兩種方式都可行,你可以根據(jù)自己的喜好進(jìn)行選擇。

Mock 測(cè)試代碼就交給你自己來完成了。

總結(jié)

如何編寫測(cè)試代碼,不僅僅是在業(yè)務(wù)代碼實(shí)現(xiàn)以后,寫單元測(cè)試時(shí)才要考慮的問題。而是在編寫業(yè)務(wù)代碼的過程中,時(shí)刻都要思考的問題。好的代碼,能夠大大降低編寫測(cè)試的難度和周期。

在編寫測(cè)試時(shí),我們應(yīng)該盡量固定所依賴對(duì)象的返回值,這就要求依賴對(duì)象的代碼能夠方便替換。如果依賴對(duì)象是一個(gè)函數(shù),我們可以將其定義為一個(gè)變量,測(cè)試時(shí)將變量替換成返回固定值的臨時(shí)對(duì)象。

我們也可以采用依賴注入的思想,將被測(cè)試代碼內(nèi)部的依賴,移動(dòng)到函數(shù)參數(shù)中來,這樣在測(cè)試時(shí),可以將依賴對(duì)象進(jìn)行替換。

在 Go 語言中,使用接口來對(duì)代碼進(jìn)行解耦,是慣用方法,同時(shí)也是解決測(cè)試依賴的突破口,使用接口,我們才有機(jī)會(huì)使用 Fake 和 Mock 測(cè)試。

此外,在我們自己編寫業(yè)務(wù)代碼時(shí),如果代碼實(shí)現(xiàn)方能夠提供 Fake object,那么也能為編寫測(cè)試代碼的人提供便利。這一點(diǎn)可以參考 K8s client-go 項(xiàng)目,K8s 團(tuán)隊(duì)在實(shí)現(xiàn) client-go 時(shí)提供了對(duì)應(yīng)的 Fake object,如果我們的代碼依賴了 client-go,那么就可以直接使用 K8s 提供的 Fake object 了,而不必自己來創(chuàng)建 Fake object,非常方便,值得借鑒。

以上就是詳解如何在Go中如何編寫出可測(cè)試的代碼的詳細(xì)內(nèi)容,更多關(guān)于Go編寫可測(cè)試代碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • golang項(xiàng)目如何上線部署到Linu服務(wù)器(方法詳解)

    golang項(xiàng)目如何上線部署到Linu服務(wù)器(方法詳解)

    這篇文章主要介紹了golang項(xiàng)目如何上線部署到Linu服務(wù)器,本文通過兩種方法給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-10-10
  • GO語言操作Elasticsearch示例分享

    GO語言操作Elasticsearch示例分享

    這篇文章主要介紹了GO語言操作Elasticsearch示例分享的相關(guān)資料,需要的朋友可以參考下
    2023-01-01
  • Go語言context?test源碼分析詳情

    Go語言context?test源碼分析詳情

    這篇文章主要介紹了Go語言context?test源碼分析詳情,關(guān)于context?test,測(cè)試對(duì)象是context包,測(cè)試包的包名是context_test,下面將對(duì)context?test源碼進(jìn)行分析,需要的朋友可以參考一下,希望對(duì)你有所幫助
    2022-02-02
  • golang之?dāng)?shù)組切片的具體用法

    golang之?dāng)?shù)組切片的具體用法

    本文主要介紹了golang之?dāng)?shù)組切片的具體用法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Go事務(wù)中止時(shí)是否真的結(jié)束事務(wù)解析

    Go事務(wù)中止時(shí)是否真的結(jié)束事務(wù)解析

    這篇文章主要為大家介紹了Go事務(wù)中止時(shí)是否真的結(jié)束事務(wù)實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • 如何利用Golang寫出高并發(fā)代碼詳解

    如何利用Golang寫出高并發(fā)代碼詳解

    今天領(lǐng)導(dǎo)問起為什么用Golang,同事回答語法簡(jiǎn)單,語言新,支持高并發(fā)。那高并發(fā)到底如何實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于如何利用Golang寫出高并發(fā)代碼的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-09-09
  • 一文帶你讀懂Golang?sync包之sync.Mutex

    一文帶你讀懂Golang?sync包之sync.Mutex

    sync.Mutex可以說是sync包的核心了,?sync.RWMutex,?sync.WaitGroup...都依賴于他,?本章我們將帶你一文讀懂sync.Mutex,快跟隨小編一起學(xué)習(xí)一下吧
    2023-04-04
  • golang版本升級(jí)的簡(jiǎn)單實(shí)現(xiàn)步驟

    golang版本升級(jí)的簡(jiǎn)單實(shí)現(xiàn)步驟

    個(gè)人感覺Go在眾多高級(jí)語言中,是在各方面都比較高效的,下面這篇文章主要給大家介紹了關(guān)于golang版本升級(jí)的簡(jiǎn)單實(shí)現(xiàn)步驟,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-02-02
  • golang gopm get -g -v 無法獲取第三方庫(kù)的解決方案

    golang gopm get -g -v 無法獲取第三方庫(kù)的解決方案

    這篇文章主要介紹了golang gopm get -g -v 無法獲取第三方庫(kù)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Windows下安裝VScode 并使用及中文配置方法

    Windows下安裝VScode 并使用及中文配置方法

    這篇文章主要介紹了Windows下安裝VScode 并使用及中文配置的方法詳解,本文通過圖文并茂的形式給大家介紹,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-03-03

最新評(píng)論