首頁 > 軟體

Go單元測試對資料庫CRUD進行Mock測試

2022-06-21 22:02:59

前言

最近在實踐中也總結了一些如何用表格驅動的方式使用 gock Mock測試外部介面呼叫。以及怎麼對GORM做mock測試,這些等這篇學完基礎後,後面再單獨寫文章給大家介紹。

這是Go語言單元測試系列教學的第3篇,介紹瞭如何使用go-sqlmockminiredis工具進行MySQLRedismock測試。

在上一篇《Go單元測試--模擬服務請求和介面返回》中,我們介紹瞭如何使用httptest和gock工具進行網路測試。

除了網路依賴之外,我們在開發中也會經常用到各種資料庫,比如常見的MySQL和Redis等。本文就分別舉例來演示如何在編寫單元測試的時候對MySQL和Redis進行mock。

go-sqlmock

sqlmock 是一個實現 sql/driver 的mock庫。它不需要建立真正的資料庫連線就可以在測試中模擬任何 sql 驅動程式的行為。使用它可以很方便的在編寫單元測試的時候mock sql語句的執行結果。

安裝

go get github.com/DATA-DOG/go-sqlmock

使用範例

這裡使用的是go-sqlmock官方檔案中提供的基礎範例程式碼。在下面的程式碼中,我們實現了一個recordStats函數用來記錄使用者瀏覽商品時產生的相關資料。具體實現的功能是在一個事務中進行以下兩次SQL操作:

  • 在表中將當前商品的瀏覽次數+1
  • product_viewers表中記錄瀏覽當前商品的使用者id
// app.go
package main
import "database/sql"
// recordStats 記錄使用者瀏覽產品資訊
func recordStats(db *sql.DB, userID, productID int64) (err error) {
 // 開啟事務
 // 操作views和product_viewers兩張表
 tx, err := db.Begin()
 if err != nil {
  return
 }
 defer func() {
  switch err {
  case nil:
   err = tx.Commit()
  default:
   tx.Rollback()
  }
 }()
 // 更新products表
 if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
  return
 }
 // product_viewers表中插入一條資料
 if _, err = tx.Exec(
  "INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)",
  userID, productID); err != nil {
  return
 }
 return
}
func main() {
 // 注意:測試的過程中並不需要真正的連線
 db, err := sql.Open("mysql", "root@/blog")
 if err != nil {
  panic(err)
 }
 defer db.Close()
 // userID為1的使用者瀏覽了productID為5的產品
 if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
  panic(err)
 }
}

現在我們需要為程式碼中的recordStats函數編寫單元測試,但是又不想在測試過程中連線真實的資料庫進行測試。這個時候我們就可以像下面範例程式碼中那樣使用sqlmock工具去mock資料庫操作。

package main
import (
 "fmt"
 "testing"
 "github.com/DATA-DOG/go-sqlmock"
)
// TestShouldUpdateStats sql執行成功的測試用例
func TestShouldUpdateStats(t *testing.T) {
 // mock一個*sql.DB物件,不需要連線真實的資料庫
 db, mock, err := sqlmock.New()
 if err != nil {
  t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
 }
 defer db.Close()
 // mock執行指定SQL語句時的返回結果
 mock.ExpectBegin()
 mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
 mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
 mock.ExpectCommit()
 // 將mock的DB物件傳入我們的函數中
 if err = recordStats(db, 2, 3); err != nil {
  t.Errorf("error was not expected while updating stats: %s", err)
 }
 // 確保期望的結果都滿足
 if err := mock.ExpectationsWereMet(); err != nil {
  t.Errorf("there were unfulfilled expectations: %s", err)
 }
}
// TestShouldRollbackStatUpdatesOnFailure sql執行失敗回滾的測試用例
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
 db, mock, err := sqlmock.New()
 if err != nil {
  t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
 }
 defer db.Close()
 mock.ExpectBegin()
 mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
 mock.ExpectExec("INSERT INTO product_viewers").
  WithArgs(2, 3).
  WillReturnError(fmt.Errorf("some error"))
 mock.ExpectRollback()
 // now we execute our method
 if err = recordStats(db, 2, 3); err == nil {
  t.Errorf("was expecting an error, but there was none")
 }
 // we make sure that all expectations were met
 if err := mock.ExpectationsWereMet(); err != nil {
  t.Errorf("there were unfulfilled expectations: %s", err)
 }
}

上面的程式碼中,定義了一個執行成功的測試用例和一個執行失敗回滾的測試用例,確保我們程式碼中的每個邏輯分支都能被測試到,提高單元測試覆蓋率的同時也保證了程式碼的健壯性。

執行單元測試,看一下最終的測試結果。

❯ go test -v
=== RUN   TestShouldUpdateStats
--- PASS: TestShouldUpdateStats (0.00s)
=== RUN   TestShouldRollbackStatUpdatesOnFailure
--- PASS: TestShouldRollbackStatUpdatesOnFailure (0.00s)
PASS
ok      golang-unit-test-demo/sqlmock_demo      0.011s

可以看到兩個測試用例的結果都符合預期,單元測試通過。

在很多使用ORM工具的場景下,也可以使用go-sqlmock庫mock資料庫操作進行測試。

miniredis

除了經常用到MySQL外,Redis在日常開發中也會經常用到。接下來的這一小節,我們將一起學習如何在單元測試中mock Redis的相關操作。

miniredis是一個純go實現的用於單元測試的redis server。它是一個簡單易用的、基於記憶體的redis替代品,它具有真正的TCP介面,你可以把它當成是redis版本的net/http/httptest

當我們為一些包含Redis操作的程式碼編寫單元測試時就可以使用它來mock Redis操作。

安裝

go get github.com/alicebob/miniredis/v2

使用範例

這裡以github.com/go-redis/redis庫為例,編寫了一個包含若干Redis操作的DoSomethingWithRedis函數。

// redis_op.go
package miniredis_demo
import (
 "context"
 "github.com/go-redis/redis/v8" // 注意匯入版本
 "strings"
 "time"
)
const (
 KeyValidWebsite = "app:valid:website:list"
)
func DoSomethingWithRedis(rdb *redis.Client, key string) bool {
 // 這裡可以是對redis操作的一些邏輯
 ctx := context.TODO()
 if !rdb.SIsMember(ctx, KeyValidWebsite, key).Val() {
  return false
 }
 val, err := rdb.Get(ctx, key).Result()
 if err != nil {
  return false
 }
 if !strings.HasPrefix(val, "https://") {
  val = "https://" + val
 }
 // 設定 blog key 五秒過期
 if err := rdb.Set(ctx, "blog", val, 5*time.Second).Err(); err != nil {
  return false
 }
 return true
}

下面的程式碼是我使用miniredis庫為DoSomethingWithRedis函數編寫的單元測試程式碼,其中miniredis不僅支援mock常用的Redis操作,還提供了很多實用的幫助函數,例如檢查key的值是否與預期相等的s.CheckGet()和幫助檢查key過期時間的s.FastForward()

// redis_op_test.go
package miniredis_demo
import (
 "github.com/alicebob/miniredis/v2"
 "github.com/go-redis/redis/v8"
 "testing"
 "time"
)
func TestDoSomethingWithRedis(t *testing.T) {
 // mock一個redis server
 s, err := miniredis.Run()
 if err != nil {
  panic(err)
 }
 defer s.Close()
 // 準備資料
 s.Set("q1mi", "liwenzhou.com")
 s.SAdd(KeyValidWebsite, "q1mi")
 // 連線mock的redis server
 rdb := redis.NewClient(&redis.Options{
  Addr: s.Addr(), // mock redis server的地址
 })
 // 呼叫函數
 ok := DoSomethingWithRedis(rdb, "q1mi")
 if !ok {
  t.Fatal()
 }
 // 可以手動檢查redis中的值是否複合預期
 if got, err := s.Get("blog"); err != nil || got != "https://liwenzhou.com" {
  t.Fatalf("'blog' has the wrong value")
 }
 // 也可以使用幫助工具檢查
 s.CheckGet(t, "blog", "https://liwenzhou.com")
 // 過期檢查
 s.FastForward(5 * time.Second) // 快進5秒
 if s.Exists("blog") {
  t.Fatal("'blog' should not have existed anymore")
 }
}

執行執行測試,檢視單元測試結果:

❯ go test -v
=== RUN   ;TestDoSomethingWithRedis
--- PASS: TestDoSomethingWithRedis (0.00s)
PASS
ok      golang-unit-test-demo/miniredis_demo    0.052s

miniredis基本上支援絕大多數的Redis命令,大家可以通過檢視檔案瞭解更多用法。

當然除了使用miniredis搭建本地redis server這種方法外,還可以使用各種打樁工具對具體方法進行打樁。在編寫單元測試時具體使用哪種mock方式還是要根據實際情況來決定。

總結

在日常工作開發中為程式碼編寫單元測試時如何處理資料庫的依賴是最常見的問題,本文介紹瞭如何使用go-sqlmockminiredis工具mock相關依賴。

接下來,我們將更進一步,詳細介紹如何在編寫單元測試時mock介面實現,更多關於Go資料庫CRUD Mock測試的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com