Go單元測試對數(shù)據(jù)庫CRUD進行Mock測試
前言
最近在實踐中也總結(jié)了一些如何用表格驅(qū)動的方式使用 gock Mock測試外部接口調(diào)用。以及怎么對GORM做mock測試,這些等這篇學(xué)完基礎(chǔ)后,后面再單獨寫文章給大家介紹。
這是Go語言單元測試系列教程的第3篇,介紹了如何使用go-sqlmock和miniredis工具進行MySQL和Redis的mock測試。
在上一篇《Go單元測試--模擬服務(wù)請求和接口返回》中,我們介紹了如何使用httptest和gock工具進行網(wǎng)絡(luò)測試。
除了網(wǎng)絡(luò)依賴之外,我們在開發(fā)中也會經(jīng)常用到各種數(shù)據(jù)庫,比如常見的MySQL和Redis等。本文就分別舉例來演示如何在編寫單元測試的時候?qū)ySQL和Redis進行mock。
go-sqlmock
sqlmock 是一個實現(xiàn) sql/driver 的mock庫。它不需要建立真正的數(shù)據(jù)庫連接就可以在測試中模擬任何 sql 驅(qū)動程序的行為。使用它可以很方便的在編寫單元測試的時候mock sql語句的執(zhí)行結(jié)果。
安裝
go?get?github.com/DATA-DOG/go-sqlmock
使用示例
這里使用的是go-sqlmock官方文檔中提供的基礎(chǔ)示例代碼。在下面的代碼中,我們實現(xiàn)了一個recordStats函數(shù)用來記錄用戶瀏覽商品時產(chǎn)生的相關(guān)數(shù)據(jù)。具體實現(xiàn)的功能是在一個事務(wù)中進行以下兩次SQL操作:
- 在表中將當(dāng)前商品的瀏覽次數(shù)+1
- 在
product_viewers表中記錄瀏覽當(dāng)前商品的用戶id
//?app.go
package?main
import?"database/sql"
//?recordStats?記錄用戶瀏覽產(chǎn)品信息
func?recordStats(db?*sql.DB,?userID,?productID?int64)?(err?error)?{
?//?開啟事務(wù)
?//?操作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表中插入一條數(shù)據(jù)
?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的產(chǎn)品
?if?err?=?recordStats(db,?1?/*some?user?id*/,?5?/*some?product?id*/);?err?!=?nil?{
??panic(err)
?}
}
現(xiàn)在我們需要為代碼中的recordStats函數(shù)編寫單元測試,但是又不想在測試過程中連接真實的數(shù)據(jù)庫進行測試。這個時候我們就可以像下面示例代碼中那樣使用sqlmock工具去mock數(shù)據(jù)庫操作。
package?main
import?(
?"fmt"
?"testing"
?"github.com/DATA-DOG/go-sqlmock"
)
//?TestShouldUpdateStats?sql執(zhí)行成功的測試用例
func?TestShouldUpdateStats(t?*testing.T)?{
?//?mock一個*sql.DB對象,不需要連接真實的數(shù)據(jù)庫
?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執(zhí)行指定SQL語句時的返回結(jié)果
?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對象傳入我們的函數(shù)中
?if?err?=?recordStats(db,?2,?3);?err?!=?nil?{
??t.Errorf("error?was?not?expected?while?updating?stats:?%s",?err)
?}
?//?確保期望的結(jié)果都滿足
?if?err?:=?mock.ExpectationsWereMet();?err?!=?nil?{
??t.Errorf("there?were?unfulfilled?expectations:?%s",?err)
?}
}
//?TestShouldRollbackStatUpdatesOnFailure?sql執(zhí)行失敗回滾的測試用例
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)
?}
}
上面的代碼中,定義了一個執(zhí)行成功的測試用例和一個執(zhí)行失敗回滾的測試用例,確保我們代碼中的每個邏輯分支都能被測試到,提高單元測試覆蓋率的同時也保證了代碼的健壯性。
執(zhí)行單元測試,看一下最終的測試結(jié)果。
? 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
可以看到兩個測試用例的結(jié)果都符合預(yù)期,單元測試通過。
在很多使用ORM工具的場景下,也可以使用go-sqlmock庫mock數(shù)據(jù)庫操作進行測試。
miniredis
除了經(jīng)常用到MySQL外,Redis在日常開發(fā)中也會經(jīng)常用到。接下來的這一小節(jié),我們將一起學(xué)習(xí)如何在單元測試中mock Redis的相關(guān)操作。
miniredis是一個純go實現(xiàn)的用于單元測試的redis server。它是一個簡單易用的、基于內(nèi)存的redis替代品,它具有真正的TCP接口,你可以把它當(dāng)成是redis版本的net/http/httptest。
當(dāng)我們?yōu)橐恍┌琑edis操作的代碼編寫單元測試時就可以使用它來mock Redis操作。
安裝
go?get?github.com/alicebob/miniredis/v2
使用示例
這里以github.com/go-redis/redis庫為例,編寫了一個包含若干Redis操作的DoSomethingWithRedis函數(shù)。
//?redis_op.go
package?miniredis_demo
import?(
?"context"
?"github.com/go-redis/redis/v8"?//?注意導(dǎo)入版本
?"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
?}
?//?設(shè)置?blog?key?五秒過期
?if?err?:=?rdb.Set(ctx,?"blog",?val,?5*time.Second).Err();?err?!=?nil?{
??return?false
?}
?return?true
}
下面的代碼是我使用miniredis庫為DoSomethingWithRedis函數(shù)編寫的單元測試代碼,其中miniredis不僅支持mock常用的Redis操作,還提供了很多實用的幫助函數(shù),例如檢查key的值是否與預(yù)期相等的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()
?//?準(zhǔn)備數(shù)據(jù)
?s.Set("q1mi",?"liwenzhou.com")
?s.SAdd(KeyValidWebsite,?"q1mi")
?//?連接mock的redis?server
?rdb?:=?redis.NewClient(&redis.Options{
??Addr:?s.Addr(),?//?mock?redis?server的地址
?})
?//?調(diào)用函數(shù)
?ok?:=?DoSomethingWithRedis(rdb,?"q1mi")
?if?!ok?{
??t.Fatal()
?}
?//?可以手動檢查redis中的值是否復(fù)合預(yù)期
?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")
?}
}
執(zhí)行執(zhí)行測試,查看單元測試結(jié)果:
? go test -v
=== RUN ;TestDoSomethingWithRedis
--- PASS: TestDoSomethingWithRedis (0.00s)
PASS
ok golang-unit-test-demo/miniredis_demo 0.052s
miniredis基本上支持絕大多數(shù)的Redis命令,大家可以通過查看文檔了解更多用法。
當(dāng)然除了使用miniredis搭建本地redis server這種方法外,還可以使用各種打樁工具對具體方法進行打樁。在編寫單元測試時具體使用哪種mock方式還是要根據(jù)實際情況來決定。
總結(jié)
在日常工作開發(fā)中為代碼編寫單元測試時如何處理數(shù)據(jù)庫的依賴是最常見的問題,本文介紹了如何使用go-sqlmock和miniredis工具mock相關(guān)依賴。
接下來,我們將更進一步,詳細介紹如何在編寫單元測試時mock接口實現(xiàn),更多關(guān)于Go數(shù)據(jù)庫CRUD Mock測試的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何使用大學(xué)教育郵箱下載golang等軟件(推薦)
這篇文章主要介紹了如何使用大學(xué)教育郵箱下載goland等軟件,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

