Go 語言中對 Redis 和 SQL 操作進行單元測試

在這篇博客文章中,我們將探討如何在 Go 語言中使用 RedisMock 庫和 SQLMock 庫進行單元測試。我們將通過一個簡單的示例項目展示如何為數據庫操作編寫單元測試,并確保我們的代碼按預期工作。

項目結構

我們的項目包含以下文件:

  • sql.go:包含數據庫操作的實現。
  • sql_test.go:包含數據庫操作的單元測試。
  • redis.go:包含 Redis 操作的實現。
  • redis_test.go:包含 Redis 操作的單元測試。

數據庫操作實現 sql.go

首先,我們定義了一個 User 結構體和一個 Data 結構體,用于封裝數據庫操作。

package main

import (
    "context"
    "time"

    "gorm.io/gorm"
)

type User struct {
    ID       int       `gorm:"primaryKey column:id"`
    Birthday time.Time `gorm:"column:birthday"`
    Username string    `gorm:"column:username"`
}

func (u User) TableName() string {
    return "users"
}

type Data struct {
    DB *gorm.DB
}

func (d *Data) Create(ctx context.Context, user *User) error {
    return d.DB.Create(user).Error
}

func (d *Data) Get(ctx context.Context, id int) (*User, error) {
    var user User
    err := d.DB.Where("`id` = ?", id).First(&user).Error
    return &user, err
}

func (d *Data) Update(ctx context.Context, user *User) error {
    return d.DB.Save(user).Error
}

func (d *Data) Delete(ctx context.Context, id int) error {
    return d.DB.Where(`id = ?`, id).Delete(User{}).Error
}

數據庫操作單元測試 sql_test.go

接下來,我們使用 SQLMock 庫為數據庫操作編寫單元測試。

package main

import (
    "context"
    "testing"
    "time"

    "github.com/DATA-DOG/go-sqlmock"
    "github.com/stretchr/testify/assert"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func initTestDB() (*gorm.DB, sqlmock.Sqlmock, error) {
    db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
    if err != nil {
        return nil, nil, err
    }

    mock.ExpectQuery("SELECT VERSION()").WillReturnRows(sqlmock.NewRows([]string{"version"}).AddRow("8.0.23"))

    gormDB, err := gorm.Open(mysql.New(mysql.Config{
        Conn: db,
    }))
    if err != nil {
        return nil, nil, err
    }

    return gormDB, mock, nil
}

func TestData_Create(t *testing.T) {
    gormDB, mock, err := initTestDB()
    assert.NoError(t, err)

    birthday, err := time.Parse("2006-01-02", "2021-01-01")
    assert.NoError(t, err)

    user := &User{
        ID:       1,
        Birthday: birthday,
        Username: "test",
    }

    mock.ExpectBegin()
    mock.ExpectExec("INSERT INTO `users` (`birthday`,`username`,`id`) VALUES (?,?,?)").
        WithArgs(user.Birthday, user.Username, user.ID).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    data := &Data{DB: gormDB}

    err = data.Create(context.Background(), user)
    assert.NoError(t, err)

    assert.NoError(t, mock.ExpectationsWereMet())
}

func TestData_Get(t *testing.T) {
    gormDB, mock, err := initTestDB()
    assert.NoError(t, err)

    userID := 1
    birthday, err := time.Parse("2006-01-02", "2021-01-01")
    assert.NoError(t, err)

    rows := sqlmock.NewRows([]string{"id", "birthday", "username"}).
        AddRow(userID, birthday, "test")

    mock.ExpectQuery("SELECT * FROM `users` WHERE `id` = ? ORDER BY `users`.`id` LIMIT ?").
        WithArgs(userID, 1).
        WillReturnRows(rows)

    data := &Data{DB: gormDB}

    user, err := data.Get(context.Background(), userID)
    assert.NoError(t, err)

    assert.Equal(t, userID, user.ID)
    assert.Equal(t, birthday, user.Birthday)
    assert.Equal(t, "test", user.Username)

    assert.NoError(t, mock.ExpectationsWereMet())
}

func TestData_Update(t *testing.T) {
    gormDB, mock, err := initTestDB()
    assert.NoError(t, err)

    birthday, err := time.Parse("2006-01-02", "2021-01-01")
    assert.NoError(t, err)

    user := &User{
        ID:       1,
        Birthday: birthday,
        Username: "test",
    }

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE `users` SET `birthday`=?,`username`=? WHERE `id` = ?").
        WithArgs(user.Birthday, user.Username, user.ID).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    data := &Data{DB: gormDB}

    err = data.Update(context.Background(), user)
    assert.NoError(t, err)
    assert.NoError(t, mock.ExpectationsWereMet())
}

func TestData_Delete(t *testing.T) {
    gormDB, mock, err := initTestDB()
    assert.NoError(t, err)

    userID := 1

    mock.ExpectBegin()
    mock.ExpectExec("DELETE FROM `users` WHERE id = ?").
        WithArgs(userID).
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    data := &Data{DB: gormDB}

    err = data.Delete(context.Background(), userID)
    assert.NoError(t, err)

    assert.NoError(t, mock.ExpectationsWereMet())
}

Redis 操作實現 redis.go

我們還定義了一個 SessionStore 結構體,用于封裝 Redis 操作。

package main

import (
    "context"
    "time"

    "github.com/redis/go-redis/v9"
)

type SessionStore struct {
    redisClient redis.UniversalClient
}

func NewSessionStore(redisClient redis.UniversalClient) *SessionStore {
    return &SessionStore{redisClient: redisClient}
}

func (s *SessionStore) GetSession(ctx context.Context, key string) (string, error) {
    return s.redisClient.Get(ctx, key).Result()
}

func (s *SessionStore) SetSession(ctx context.Context, key, value string, expiration time.Duration) error {
    return s.redisClient.Set(ctx, key, value, expiration).Err()
}

Redis 操作單元測試 redis_test.go

最后,我們使用 Redismock 庫為 Redis 操作編寫單元測試。

import (
    "context"
    "testing"
    "time"

    "github.com/go-redis/redismock/v9"
    "github.com/stretchr/testify/assert"
)

func TestSessionStore_GetSession(t *testing.T) {
    db, mock := redismock.NewClientMock()

    store := NewSessionStore(db)

    key := "test-key"
    value := "test-value"
    mock.ExpectGet(key).SetVal(value)

    result, err := store.GetSession(context.Background(), key)

    assert.Nil(t, err)
    assert.Equal(t, value, result)
    assert.NoError(t, mock.ExpectationsWereMet())
}

func TestSessionStore_SetSession(t *testing.T) {
    db, mock := redismock.NewClientMock()

    store := NewSessionStore(db)

    key := "test-key"
    value := "test-value"
    expiration := 10 * time.Minute
    mock.ExpectSet(key, value, expiration).SetVal("OK")

    err := store.SetSession(context.Background(), key, value, expiration)
    assert.NoError(t, err)
    assert.NoError(t, mock.ExpectationsWereMet())
}

執行單元測試

通過go test -v ./...對當前項目進行單元測試。結果如下:

image.png

總結

通過本文,我們展示了如何使用 SQLMock 和 Redismock 庫為 Go 語言中的數據庫和 Redis 操作編寫單元測試。希望這篇文章能幫助你更好地理解和應用這些工具,提高代碼的可靠性和可維護性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370