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

詳解在Go語言單元測試中如何解決Redis存儲依賴問題

 更新時間:2023年08月07日 09:19:02   作者:江湖十年  
在編寫單元測試時,除了?MySQL?這個外部存儲依賴,Redis?應(yīng)該是另一個最為常見的外部存儲依賴了,本文就來講解下如何解決?Redis?外部依賴,文章通過代碼示例介紹的非常詳細,需要的朋友可以參考下

登錄程序示例

在 Web 開發(fā)中,登錄需求是一個較為常見的功能。假設(shè)我們有一個 Login 函數(shù),可以實現(xiàn)用戶登錄功能。它接收用戶手機號 + 短信驗證碼,然后根據(jù)手機號從 Redis 中獲取保存的驗證碼(驗證碼通常是在發(fā)送驗證碼這一操作時保存的),如果 Redis 中驗證碼與用戶輸入的驗證碼相同,則表示用戶信息正確,然后生成一個隨機 token 作為登錄憑證,之后先將 token 寫入 Redis 中,再返回給用戶,表示登錄操作成功。

程序代碼實現(xiàn)如下:

func Login(mobile, smsCode string, rdb *redis.Client, generateToken func(int) (string, error)) (string, error) {
	ctx := context.Background()
	// 查找驗證碼
	captcha, err := GetSmsCaptchaFromRedis(ctx, rdb, mobile)
	if err != nil {
		if err == redis.Nil {
			return "", fmt.Errorf("invalid sms code or expired")
		}
		return "", err
	}
	if captcha != smsCode {
		return "", fmt.Errorf("invalid sms code")
	}
	// 登錄,生成 token 并寫入 Redis
	token, _ := generateToken(32)
	err = SetAuthTokenToRedis(ctx, rdb, token, mobile)
	if err != nil {
		return "", err
	}
	return token, nil
}

Login 函數(shù)有 4 個參數(shù),分別是用戶手機號、驗證碼、Redis 客戶端連接對象、輔助生成隨機 token 的函數(shù)。

Redis 客戶端連接對象 *redis.Client 屬于 github.com/redis/go-redis/v9 包。

我們可以使用如下方式獲得:

func NewRedisClient() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
	})
}

generateToken 用來生成隨機長度 token,定義如下:

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

我們還要為 Redis 操作編寫幾個函數(shù),用來存取 Redis 中的驗證碼和 token:

var (
	smsCaptchaExpire    = 5 * time.Minute
	smsCaptchaKeyPrefix = "sms:captcha:%s"
	authTokenExpire    = 24 * time.Hour
	authTokenKeyPrefix = "auth:token:%s"
)
func SetSmsCaptchaToRedis(ctx context.Context, redis *redis.Client, mobile, captcha string) error {
	key := fmt.Sprintf(smsCaptchaKeyPrefix, mobile)
	return redis.Set(ctx, key, captcha, smsCaptchaExpire).Err()
}
func GetSmsCaptchaFromRedis(ctx context.Context, redis *redis.Client, mobile string) (string, error) {
	key := fmt.Sprintf(smsCaptchaKeyPrefix, mobile)
	return redis.Get(ctx, key).Result()
}
func SetAuthTokenToRedis(ctx context.Context, redis *redis.Client, token, mobile string) error {
	key := fmt.Sprintf(authTokenKeyPrefix, mobile)
	return redis.Set(ctx, key, token, authTokenExpire).Err()
}
func GetAuthTokenFromRedis(ctx context.Context, redis *redis.Client, token string) (string, error) {
	key := fmt.Sprintf(authTokenKeyPrefix, token)
	return redis.Get(ctx, key).Result()
}

Login 函數(shù)使用方式如下:

func main() {
	rdb := NewRedisClient()
	token, err := Login("13800001111", "123456", rdb, GenerateToken)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(token)
}

使用 redismock 測試

現(xiàn)在,我們要對 Login 函數(shù)進行單元測試。

Login 函數(shù)依賴了 *redis.Client 以及 generateToken 函數(shù)。

由于我們設(shè)計的代碼是 Login 函數(shù)直接依賴了 *redis.Client ,沒有通過接口來解耦,所以不能使用 gomock 工具來生成 Mock 代碼。

不過,我們可以看看 go-redis 包的源碼倉庫有沒有什么線索。

很幸運,在 go-redis 包的 README.md 文檔里,我們可以看到一個 Redis Mock 鏈接:

image.png

點擊進去,我們就來到了一個叫 redismock 的倉庫, redismock 為我們實現(xiàn)了一個模擬的 Redis 客戶端。

使用如下方式安裝 redismock

$ go get github.com/go-redis/redismock/v9 

使用如下方式導(dǎo)入 redismock

import "github.com/go-redis/redismock/v9" 

切記安裝和導(dǎo)入的 redismock 包版本要與 go-redis 包版本一致,這里都為 v9 。

可以通過如下方式快速創(chuàng)建一個 Redis 客戶端 rdb ,以及客戶端 Mock 對象 mock

rdb, mock := redismock.NewClientMock() 

在測試代碼中,調(diào)用 Login 函數(shù)時,就可以使用這個 rdb 作為 Redis 客戶端了。

mock 對象提供了 ExpectXxx 方法,用來指定 rdb 客戶端預(yù)期會調(diào)用哪些方法以及對應(yīng)參數(shù)。

// login success
mock.ExpectGet("sms:captcha:13800138000").SetVal("123456")
mock.ExpectSet("auth:token:Ta5EVtRgUD-HFmRwrujAwKZnx247lFfe", "13800138000", 24*time.Hour).SetVal("OK")

mock.ExpectGet 表示期待一個 Redis Get 操作,Key 為 sms:captcha:13800138000 , SetVal("123456") 用來設(shè)置當(dāng)前 Get 操作返回值為 123456 。

同理, mock.ExpectSet 表示期待一個 Redis Set 操作,Key 為 auth:token:Ta5EVtRgUD-HFmRwrujAwKZnx247lFfe ,Value 為 13800138000 ,過期時間為 24*time.Hour ,返回 OK 表示這個 Set 操作成功。

以上指定的兩個預(yù)期方法調(diào)用,是用來匹配 Login 成功時的用例。

Login 函數(shù)還有兩種失敗情況,當(dāng)通過 GetSmsCaptchaFromRedis 函數(shù)查詢 Redis 中驗證碼不存在時,返回 invalid sms code or expired 錯誤。當(dāng)從 Redis 中查詢的驗證碼與用戶傳遞進來的驗證碼不匹配時,返回 invalid sms code 錯誤。

這兩種用例可以按照如下方式模擬:

// invalid sms code or expired
mock.ExpectGet("sms:captcha:13900139000").RedisNil()
// invalid sms code
mock.ExpectGet("sms:captcha:13700137000").SetVal("123123")

現(xiàn)在,我們已經(jīng)解決了 Redis 依賴,還需要解決 generateToken 函數(shù)依賴。

這時候 Fake object 就派上用場了:

func fakeGenerateToken(int) (string, error) {
	return "Ta5EVtRgUD-HFmRwrujAwKZnx247lFfe", nil
}

我們使用 fakeGenerateToken 函數(shù)來替代 GenerateToken 函數(shù),這樣生成的 token 就固定下來了,方便測試。

Login 函數(shù)完整單元測試代碼實現(xiàn)如下:

func TestLogin(t *testing.T) {
	// mock redis client
	rdb, mock := redismock.NewClientMock()
	// login success
	mock.ExpectGet("sms:captcha:13800138000").SetVal("123456")
	mock.ExpectSet("auth:token:Ta5EVtRgUD-HFmRwrujAwKZnx247lFfe", "13800138000", 24*time.Hour).SetVal("OK")
	// invalid sms code or expired
	mock.ExpectGet("sms:captcha:13900139000").RedisNil()
	// invalid sms code
	mock.ExpectGet("sms:captcha:13700137000").SetVal("123123")
	type args struct {
		mobile  string
		smsCode string
	}
	tests := []struct {
		name    string
		args    args
		want    string
		wantErr string
	}{
		{
			name: "login success",
			args: args{
				mobile:  "13800138000",
				smsCode: "123456",
			},
			want: "Ta5EVtRgUD-HFmRwrujAwKZnx247lFfe",
		},
		{
			name: "invalid sms code or expired",
			args: args{
				mobile:  "13900139000",
				smsCode: "123459",
			},
			wantErr: "invalid sms code or expired",
		},
		{
			name: "invalid sms code",
			args: args{
				mobile:  "13700137000",
				smsCode: "123457",
			},
			wantErr: "invalid sms code",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Login(tt.args.mobile, tt.args.smsCode, rdb, fakeGenerateToken)
			if tt.wantErr != "" {
				assert.Error(t, err)
				assert.Equal(t, tt.wantErr, err.Error())
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.want, got)
			}
		})
	}
}

這里使用了表格測試,提供了 3 個測試用例,覆蓋了登錄成功、驗證碼無效或過期、驗證碼無效 3 種場景。

使用 go test 來執(zhí)行測試函數(shù):

$ go test -v .                 
=== RUN   TestLogin
=== RUN   TestLogin/login_success
=== RUN   TestLogin/invalid_sms_code_or_expired
=== RUN   TestLogin/invalid_sms_code
--- PASS: TestLogin (0.00s)
    --- PASS: TestLogin/login_success (0.00s)
    --- PASS: TestLogin/invalid_sms_code_or_expired (0.00s)
    --- PASS: TestLogin/invalid_sms_code (0.00s)
PASS
ok      github.com/jianghushinian/blog-go-example/test/redis    0.152s

測試通過。

Login 函數(shù)將 *redis.Client generateToken 這兩個外部依賴定義成了函數(shù)參數(shù),而不是在函數(shù)內(nèi)部直接使用這兩個依賴。

這主要參考了「依賴注入」的思想,將依賴當(dāng)作參數(shù)傳入,而不是在函數(shù)內(nèi)部直接引用。

這樣,我們才有機會使用 Fake 對象 fakeGenerateToken 來替代真實對象 GenerateToken 。

而對于 *redis.Client ,我們也能夠使用 redismock 提供的 Mock 對象來替代。

redismock 不僅能夠模擬 RedisClient,它還支持模擬 RedisCluster,更多使用示例可以在官方示例中查看。

使用 Testcontainers 測試

雖然我們使用 redismock 提供的 Mock 對象解決了 Login 函數(shù)對 *redis.Client 的依賴問題。

但這需要運氣,當(dāng)我們使用其他數(shù)據(jù)庫時,也許找不到現(xiàn)成的 Mock 庫。

此時,我們還有另一個強大的工具「容器」可以使用。

如果程序所依賴的某個外部服務(wù),實在找不到現(xiàn)成的 Mock 工具,自己實現(xiàn) Fack object 又比較麻煩,這時就可以考慮使用容器來運行一個真正的外部服務(wù)了。

Testcontainers 就是用來解決這個問題的,我們可以用它來啟動容器,運行任何外部服務(wù)。

Testcontainers 非常強大,不僅支持 Go 語言,還支持 Java、Python、Rust 等其他主流編程語言。它可以很容易地創(chuàng)建和清理基于容器的依賴,常被用于集成測試和冒煙測試。所以這也提醒我們在單元測試中慎用,因為容器也是一個外部依賴。

我們可以按照如下方式使用 Testcontainers 在容器中啟動一個 Redis 服務(wù):

import (
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)
// 在容器中運行一個 Redis 服務(wù)
func RunWithRedisInContainer() (*redis.Client, func()) {
	ctx := context.Background()
	// 創(chuàng)建容器請求參數(shù)
	req := testcontainers.ContainerRequest{
		Image:        "redis:6.0.20-alpine",                      // 指定容器鏡像
		ExposedPorts: []string{"6379/tcp"},                       // 指定容器暴露端口
		WaitingFor:   wait.ForLog("Ready to accept connections"), // 等待輸出容器 Ready 日志
	}
	// 創(chuàng)建 Redis 容器
	redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		panic(fmt.Sprintf("failed to start container: %s", err.Error()))
	}
	// 獲取容器中 Redis 連接地址,e.g. localhost:50351
	endpoint, err := redisC.Endpoint(ctx, "") // 如果暴露多個端口,可以指定第二個參數(shù)
	if err != nil {
		panic(fmt.Sprintf("failed to get endpoint: %s", err.Error()))
	}
	// 連接容器中的 Redis
	client := redis.NewClient(&redis.Options{
		Addr: endpoint,
	})
	// 返回 Redis Client 和 cleanup 函數(shù)
	return client, func() {
		if err := redisC.Terminate(ctx); err != nil {
			panic(fmt.Sprintf("failed to terminate container: %s", err.Error()))
		}
	}
}

代碼中我寫了比較詳細的注釋,就不帶大家一一解釋代碼內(nèi)容了。

我們可以將容器的啟動和釋放操作放到 TestMain 函數(shù)中,這樣在執(zhí)行測試函數(shù)之前先啟動容器,然后進行測試,最后在測試結(jié)束時銷毀容器。

var rdbClient *redis.Client
func TestMain(m *testing.M) {
	client, f := RunWithRedisInContainer()
	defer f()
	rdbClient = client
	m.Run()
}

使用容器編寫的 Login 單元測試函數(shù)如下:

func TestLogin_by_container(t *testing.T) {
	// 準(zhǔn)備測試數(shù)據(jù)
	err := SetSmsCaptchaToRedis(context.Background(), rdbClient, "18900001111", "123456")
	assert.NoError(t, err)
	// 測試登錄成功情況
	gotToken, err := Login("18900001111", "123456", rdbClient, GenerateToken)
	assert.NoError(t, err)
	assert.Equal(t, 32, len(gotToken))
	// 檢查 Redis 中是否存在 token
	gotMobile, err := GetAuthTokenFromRedis(context.Background(), rdbClient, gotToken)
	assert.NoError(t, err)
	assert.Equal(t, "18900001111", gotMobile)
}

現(xiàn)在因為有了容器的存在,我們有了一個真實的 Redis 服務(wù)。所以編寫測試代碼時,無需再考慮如何模擬 Redis 客戶端,只需要使用通過 RunWithRedisInContainer() 函數(shù)創(chuàng)建的真實客戶端 rdbClient 即可,一切操作都是真實的。

并且,我們也不再需要實現(xiàn) fakeGenerateToken 函數(shù)來固定生成的 token,直接使用 GenerateToken 生成真實的隨機 token 即可。想要驗證得到的 token 是否正確,可以直接從 Redis 服務(wù)中讀取。

執(zhí)行測試前,確保主機上已經(jīng)安裝了 Docker, Testcontainers 會使用主機上的 Docker 來運行容器。

使用 go test 來執(zhí)行測試函數(shù):

$ go test -v -run="TestLogin_by_container"
2023/07/17 22:59:34 github.com/testcontainers/testcontainers-go - Connected to docker: 
  Server Version: 20.10.21
  API Version: 1.41
  Operating System: Docker Desktop
  Total Memory: 7851 MB
2023/07/17 22:59:34 ?? Creating container for image docker.io/testcontainers/ryuk:0.5.1
2023/07/17 22:59:34 ? Container created: 92e327ad7b70
2023/07/17 22:59:34 ?? Starting container: 92e327ad7b70
2023/07/17 22:59:35 ? Container started: 92e327ad7b70
2023/07/17 22:59:35 ?? Waiting for container id 92e327ad7b70 image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms}
2023/07/17 22:59:35 ?? Creating container for image redis:6.0.20-alpine
2023/07/17 22:59:35 ? Container created: 2b5e40d40af0
2023/07/17 22:59:35 ?? Starting container: 2b5e40d40af0
2023/07/17 22:59:35 ? Container started: 2b5e40d40af0
2023/07/17 22:59:35 ?? Waiting for container id 2b5e40d40af0 image: redis:6.0.20-alpine. Waiting for: &{timeout:<nil> Log:Ready to accept connections Occurrence:1 PollInterval:100ms}
=== RUN   TestLogin_by_container
--- PASS: TestLogin_by_container (0.00s)
PASS
2023/07/17 22:59:36 ?? Terminating container: 2b5e40d40af0
2023/07/17 22:59:36 ?? Container terminated: 2b5e40d40af0
ok      github.com/jianghushinian/blog-go-example/test/redis    1.545s

測試通過。

根據(jù)輸出日志可以發(fā)現(xiàn),我們的確在主機上創(chuàng)建了一個 Redis 容器來運行 Redis 服務(wù):

Creating container for image redis:6.0.20-alpine 

容器 ID 為 2b5e40d40af0

Container created: 2b5e40d40af0 

并且測試結(jié)束后清理了容器:

Container terminated: 2b5e40d40af0 

以上,我們就利用容器技術(shù),為 Login 函數(shù)登錄成功情況編寫了一個測試用例,登錄失敗情況的測試用例就留做作業(yè)交給你自己來完成吧。

總結(jié)

本文向大家介紹了在 Go 中編寫單元測試時,如何解決 Redis 外部依賴的問題。

值得慶幸的是 redismock 包提供了模擬的 Redis 客戶端,方便我們在測試過程中替換 Redis 外部依賴。

但有些時候,我們可能找不到這種現(xiàn)成的第三方包。 Testcontainers 庫則為我們提供了另一種解決方案,運行一個真實的容器,以此來提供 Redis 服務(wù)。

不過,雖然 Testcontainers 足夠強大,但不到萬不得已,不推薦使用。畢竟我們又引入了容器這個外部依賴,如果網(wǎng)絡(luò)情況不好,如何拉取 Redis 鏡像也是需要解決的問題。

更好的解決辦法,是我們在編寫代碼時,就要考慮如何寫出可測試的代碼,好的代碼設(shè)計,能夠大大降低編寫測試的難度。

以上就是詳解在Go語言單元測試中如何解決Redis存儲依賴問題的詳細內(nèi)容,更多關(guān)于Go單元測試解決Redis存儲依賴的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go Module依賴管理的實現(xiàn)

    Go Module依賴管理的實現(xiàn)

    Go Module是Go語言的官方依賴管理解決方案,其提供了一種簡單、可靠的方式來管理項目的依賴關(guān)系,本文主要介紹了Go Module依賴管理的實現(xiàn),感興趣的可以了解一下
    2024-06-06
  • go select的用法

    go select的用法

    本文主要介紹了go select的用法,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • go語言fasthttp使用實例小結(jié)

    go語言fasthttp使用實例小結(jié)

    fasthttp?是一個使用?Go?語言開發(fā)的?HTTP?包,主打高性能,針對?HTTP?請求響應(yīng)流程中的?hot?path?代碼進行了優(yōu)化,下面我們就來介紹go語言fasthttp使用實例小結(jié),感興趣的朋友跟隨小編一起看看吧
    2024-03-03
  • 詳解Go語言中關(guān)于包導(dǎo)入必學(xué)的 8 個知識點

    詳解Go語言中關(guān)于包導(dǎo)入必學(xué)的 8 個知識點

    這篇文章主要介紹了詳解Go語言中關(guān)于包導(dǎo)入必學(xué)的 8 個知識點,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-08-08
  • Go基礎(chǔ)系列:Go切片(分片)slice詳解

    Go基礎(chǔ)系列:Go切片(分片)slice詳解

    這篇文章主要介紹了Go語言中的切片(分片)slice詳細說明?,需要的朋友可以參考下
    2022-04-04
  • 詳解Go語言中select語句的常見用法

    詳解Go語言中select語句的常見用法

    這篇文章主要是來和大家介紹一下Go語言中select?語句的常見用法,以及在使用過程中的注意事項,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下
    2023-07-07
  • go實現(xiàn)redigo的簡單操作

    go實現(xiàn)redigo的簡單操作

    golang操作redis主要有兩個庫,go-redis和redigo,今天我們就一起來介紹一下redigo的實現(xiàn)方法,需要的朋友可以參考下
    2018-07-07
  • Go實現(xiàn)自己的網(wǎng)絡(luò)流量解析和行為檢測引擎原理

    Go實現(xiàn)自己的網(wǎng)絡(luò)流量解析和行為檢測引擎原理

    這篇文章主要為大家介紹了Go實現(xiàn)自己的網(wǎng)絡(luò)流量解析和行為檢測引擎原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-11-11
  • go基于Gin框架的HTTP接口限速實踐

    go基于Gin框架的HTTP接口限速實踐

    HTTP接口在各個業(yè)務(wù)模塊之間扮演著重要的角色,本文主要介紹了go基于Gin框架的HTTP接口限速實踐,具有一定的參考價值,感興趣的可以了解一下
    2023-09-09
  • Go語言colly框架的快速入門

    Go語言colly框架的快速入門

    Python?中非常知名的爬蟲框架有Scrapy,Go?中也有一些?star?數(shù)較高的爬蟲框架,colly就是其中的佼佼者,它?API?簡潔,性能優(yōu)良,開箱即用,今天就來快速學(xué)習(xí)一下吧
    2023-07-07

最新評論