Go單元測試對數(shù)據(jù)庫CRUD進行Mock測試
前言
最近在實踐中也總結(jié)了一些如何用表格驅(qū)動的方式使用 gock Mock測試外部接口調(diào)用。以及怎么對GORM做mock測試,這些等這篇學完基礎(chǔ)后,后面再單獨寫文章給大家介紹。
這是Go語言單元測試系列教程的第3篇,介紹了如何使用go-sqlmock
和miniredis
工具進行MySQL
和Redis
的mock
測試。
在上一篇《Go單元測試--模擬服務請求和接口返回》中,我們介紹了如何使用httptest和gock工具進行網(wǎng)絡測試。
除了網(wǎng)絡依賴之外,我們在開發(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)的功能是在一個事務中進行以下兩次SQL操作:
- 在表中將當前商品的瀏覽次數(shù)+1
- 在
product_viewers
表中記錄瀏覽當前商品的用戶id
//?app.go package?main import?"database/sql" //?recordStats?記錄用戶瀏覽產(chǎn)品信息 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表中插入一條數(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é)果都符合預期,單元測試通過。
在很多使用ORM工具的場景下,也可以使用go-sqlmock
庫mock數(shù)據(jù)庫操作進行測試。
miniredis
除了經(jīng)常用到MySQL外,Redis在日常開發(fā)中也會經(jīng)常用到。接下來的這一小節(jié),我們將一起學習如何在單元測試中mock Redis的相關(guān)操作。
miniredis是一個純go實現(xiàn)的用于單元測試的redis server。它是一個簡單易用的、基于內(nèi)存的redis替代品,它具有真正的TCP接口,你可以把它當成是redis版本的net/http/httptest
。
當我們?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"?//?注意導入版本 ?"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的值是否與預期相等的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() ?//?準備數(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中的值是否復合預期 ?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命令,大家可以通過查看文檔了解更多用法。
當然除了使用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)文章!