<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近在實踐中也總結了一些如何用表格驅動的方式使用 gock Mock測試外部介面呼叫。以及怎麼對GORM做mock測試,這些等這篇學完基礎後,後面再單獨寫文章給大家介紹。
這是Go語言單元測試系列教學的第3篇,介紹瞭如何使用go-sqlmock
和miniredis
工具進行MySQL
和Redis
的mock
測試。
在上一篇《Go單元測試--模擬服務請求和介面返回》中,我們介紹瞭如何使用httptest和gock工具進行網路測試。
除了網路依賴之外,我們在開發中也會經常用到各種資料庫,比如常見的MySQL和Redis等。本文就分別舉例來演示如何在編寫單元測試的時候對MySQL和Redis進行mock。
sqlmock 是一個實現 sql/driver
的mock庫。它不需要建立真正的資料庫連線就可以在測試中模擬任何 sql 驅動程式的行為。使用它可以很方便的在編寫單元測試的時候mock sql語句的執行結果。
go get github.com/DATA-DOG/go-sqlmock
這裡使用的是go-sqlmock
官方檔案中提供的基礎範例程式碼。在下面的程式碼中,我們實現了一個recordStats
函數用來記錄使用者瀏覽商品時產生的相關資料。具體實現的功能是在一個事務中進行以下兩次SQL操作:
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資料庫操作進行測試。
除了經常用到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-sqlmock
和miniredis
工具mock相關依賴。
接下來,我們將更進一步,詳細介紹如何在編寫單元測試時mock介面實現,更多關於Go資料庫CRUD Mock測試的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45