欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

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

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

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

假設(shè)我們編寫了一個 Login 函數(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ù)生成一個 32 位長度的隨機 token 作為認證信息,最終返回 token

這個函數(shù)只編寫了大體框架,具體細節(jié)沒有實現(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ù)編寫單元測試,可以寫出如下測試代碼:

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 長度,而無法斷言 token 具體內(nèi)容,因為 GenerateToken 函數(shù)每次隨機生成的 token 值是不一樣的。

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

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

要解決這個問題,我們需要重新定義下 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ù)名,而是一個變量名,這個變量指向了一個匿名函數(shù)。

現(xiàn)在我們就有機會在測試 Login 的時候,將 GenerateToken 變量進行替換,實現(xiàn)一個只會返回固定輸出的 GenerateToken 函數(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ù),在測試文件執(zhí)行一開始就替換了 GenerateToken 變量的指向,新的匿名函數(shù)返回固定的 token。這樣一來,在測試時 Login 函數(shù)內(nèi)部調(diào)用的就是 GenerateToken 變量所指向的函數(shù)了,其返回值已經(jīng)被固定,因此,我們可以對其進行斷言操作。

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

現(xiàn)在我們有一個 GenerateJWT 函數(shù),用來生成 JSON Web Token,其實現(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)
}

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

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

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))
}

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

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

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

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

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),所謂的依賴注入,就是當 GenerateJWT 函數(shù)依賴當前時間時,我們不再通過 GenerateJWT 函數(shù)內(nèi)部直接調(diào)用 time.Now() 來獲取,而是使用參數(shù)(nowFunc)的方式,將 time.Now 函數(shù)傳遞進來,當函數(shù)內(nèi)部需要獲取當前時間時,就調(diào)用傳遞進來的函數(shù)參數(shù)。

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

現(xiàn)在實現(xiàn)的單元測試代碼就可以斷言生成的 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)
}

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

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

對于 GenerateJWT 函數(shù),我還編寫了一個 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 會對其進行重新排序,排序結(jié)果影響簽名結(jié)果,簽名結(jié)果驗證網(wǎng)址:https://jwt.io/
		"issuer":    j.issuer,
		"issuedAt":  nowSec,
		"expiresAt": nowSec + int64(expire.Seconds()),
		"subject":   userId,
	})
	return token.SignedString(j.privateKey)
}

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

使用接口來解耦代碼

我們有一個 GetChangeLog 函數(shù)可以返回項目的 ChangeLog,實現(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ù)接收一個文件對象 *os.File,使用 io.ReadAll(f) 從文件對象中讀取全部的 ChangeLog 內(nèi)容并返回。

如果要測試這個函數(shù),我們需要在單元測試中創(chuàng)建一個臨時文件,測試完成后還要對臨時文件進行清理,實現(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)
}

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

其實,我們還有更好的方式來實現(xiàn)這個 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
}

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

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

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

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

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ù)從字符串復制到 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)建一個真實的文件對象,而是提供一個實現(xiàn)了 io.Reader 接口的 fakeReader 對象。

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

此外,因為使用了接口來解耦,我們還可以使用 Mock 技術(shù)來編寫測試代碼。

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

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

type IReader io.Reader

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

還可以對 io.Reader 進行一層包裝:

type ReaderWrapper interface {
	io.Reader
}

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

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

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

總結(jié)

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

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

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

在 Go 語言中,使用接口來對代碼進行解耦,是慣用方法,同時也是解決測試依賴的突破口,使用接口,我們才有機會使用 Fake 和 Mock 測試。

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

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

相關(guān)文章

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

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

    這篇文章主要介紹了golang項目如何上線部署到Linu服務器,本文通過兩種方法給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    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,測試對象是context包,測試包的包名是context_test,下面將對context?test源碼進行分析,需要的朋友可以參考一下,希望對你有所幫助
    2022-02-02
  • golang之數(shù)組切片的具體用法

    golang之數(shù)組切片的具體用法

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

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

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

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

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

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

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

    golang版本升級的簡單實現(xiàn)步驟

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

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

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

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

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

最新評論