Go單元測(cè)試對(duì)數(shù)據(jù)庫(kù)CRUD進(jìn)行Mock測(cè)試
前言
最近在實(shí)踐中也總結(jié)了一些如何用表格驅(qū)動(dòng)的方式使用 gock Mock測(cè)試外部接口調(diào)用。以及怎么對(duì)GORM做mock測(cè)試,這些等這篇學(xué)完基礎(chǔ)后,后面再單獨(dú)寫文章給大家介紹。
這是Go語(yǔ)言單元測(cè)試系列教程的第3篇,介紹了如何使用go-sqlmock
和miniredis
工具進(jìn)行MySQL
和Redis
的mock
測(cè)試。
在上一篇《Go單元測(cè)試--模擬服務(wù)請(qǐng)求和接口返回》中,我們介紹了如何使用httptest和gock工具進(jìn)行網(wǎng)絡(luò)測(cè)試。
除了網(wǎng)絡(luò)依賴之外,我們?cè)陂_(kāi)發(fā)中也會(huì)經(jīng)常用到各種數(shù)據(jù)庫(kù),比如常見(jiàn)的MySQL和Redis等。本文就分別舉例來(lái)演示如何在編寫單元測(cè)試的時(shí)候?qū)ySQL和Redis進(jìn)行mock。
go-sqlmock
sqlmock 是一個(gè)實(shí)現(xiàn) sql/driver
的mock庫(kù)。它不需要建立真正的數(shù)據(jù)庫(kù)連接就可以在測(cè)試中模擬任何 sql 驅(qū)動(dòng)程序的行為。使用它可以很方便的在編寫單元測(cè)試的時(shí)候mock sql語(yǔ)句的執(zhí)行結(jié)果。
安裝
go?get?github.com/DATA-DOG/go-sqlmock
使用示例
這里使用的是go-sqlmock
官方文檔中提供的基礎(chǔ)示例代碼。在下面的代碼中,我們實(shí)現(xiàn)了一個(gè)recordStats
函數(shù)用來(lái)記錄用戶瀏覽商品時(shí)產(chǎn)生的相關(guān)數(shù)據(jù)。具體實(shí)現(xiàn)的功能是在一個(gè)事務(wù)中進(jìn)行以下兩次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)?{ ?//?開(kāi)啟事務(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()?{ ?//?注意:測(cè)試的過(guò)程中并不需要真正的連接 ?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ù)編寫單元測(cè)試,但是又不想在測(cè)試過(guò)程中連接真實(shí)的數(shù)據(jù)庫(kù)進(jìn)行測(cè)試。這個(gè)時(shí)候我們就可以像下面示例代碼中那樣使用sqlmock
工具去mock數(shù)據(jù)庫(kù)操作。
package?main import?( ?"fmt" ?"testing" ?"github.com/DATA-DOG/go-sqlmock" ) //?TestShouldUpdateStats?sql執(zhí)行成功的測(cè)試用例 func?TestShouldUpdateStats(t?*testing.T)?{ ?//?mock一個(gè)*sql.DB對(duì)象,不需要連接真實(shí)的數(shù)據(jù)庫(kù) ?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語(yǔ)句時(shí)的返回結(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對(duì)象傳入我們的函數(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í)行失敗回滾的測(cè)試用例 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) ?} }
上面的代碼中,定義了一個(gè)執(zhí)行成功的測(cè)試用例和一個(gè)執(zhí)行失敗回滾的測(cè)試用例,確保我們代碼中的每個(gè)邏輯分支都能被測(cè)試到,提高單元測(cè)試覆蓋率的同時(shí)也保證了代碼的健壯性。
執(zhí)行單元測(cè)試,看一下最終的測(cè)試結(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
可以看到兩個(gè)測(cè)試用例的結(jié)果都符合預(yù)期,單元測(cè)試通過(guò)。
在很多使用ORM工具的場(chǎng)景下,也可以使用go-sqlmock
庫(kù)mock數(shù)據(jù)庫(kù)操作進(jìn)行測(cè)試。
miniredis
除了經(jīng)常用到MySQL外,Redis在日常開(kāi)發(fā)中也會(huì)經(jīng)常用到。接下來(lái)的這一小節(jié),我們將一起學(xué)習(xí)如何在單元測(cè)試中mock Redis的相關(guān)操作。
miniredis是一個(gè)純go實(shí)現(xiàn)的用于單元測(cè)試的redis server。它是一個(gè)簡(jiǎn)單易用的、基于內(nèi)存的redis替代品,它具有真正的TCP接口,你可以把它當(dāng)成是redis版本的net/http/httptest
。
當(dāng)我們?yōu)橐恍┌琑edis操作的代碼編寫單元測(cè)試時(shí)就可以使用它來(lái)mock Redis操作。
安裝
go?get?github.com/alicebob/miniredis/v2
使用示例
這里以github.com/go-redis/redis
庫(kù)為例,編寫了一個(gè)包含若干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?{ ?//?這里可以是對(duì)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?五秒過(guò)期 ?if?err?:=?rdb.Set(ctx,?"blog",?val,?5*time.Second).Err();?err?!=?nil?{ ??return?false ?} ?return?true }
下面的代碼是我使用miniredis
庫(kù)為DoSomethingWithRedis
函數(shù)編寫的單元測(cè)試代碼,其中miniredis
不僅支持mock常用的Redis操作,還提供了很多實(shí)用的幫助函數(shù),例如檢查key的值是否與預(yù)期相等的s.CheckGet()
和幫助檢查key過(guò)期時(shí)間的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一個(gè)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() ?} ?//?可以手動(dòng)檢查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") ?//?過(guò)期檢查 ?s.FastForward(5?*?time.Second)?//?快進(jìn)5秒 ?if?s.Exists("blog")?{ ??t.Fatal("'blog'?should?not?have?existed?anymore") ?} }
執(zhí)行執(zhí)行測(cè)試,查看單元測(cè)試結(jié)果:
? go test -v
=== RUN ;TestDoSomethingWithRedis
--- PASS: TestDoSomethingWithRedis (0.00s)
PASS
ok golang-unit-test-demo/miniredis_demo 0.052s
miniredis
基本上支持絕大多數(shù)的Redis命令,大家可以通過(guò)查看文檔了解更多用法。
當(dāng)然除了使用miniredis
搭建本地redis server這種方法外,還可以使用各種打樁工具對(duì)具體方法進(jìn)行打樁。在編寫單元測(cè)試時(shí)具體使用哪種mock方式還是要根據(jù)實(shí)際情況來(lái)決定。
總結(jié)
在日常工作開(kāi)發(fā)中為代碼編寫單元測(cè)試時(shí)如何處理數(shù)據(jù)庫(kù)的依賴是最常見(jiàn)的問(wèn)題,本文介紹了如何使用go-sqlmock
和miniredis
工具mock相關(guān)依賴。
接下來(lái),我們將更進(jìn)一步,詳細(xì)介紹如何在編寫單元測(cè)試時(shí)mock接口實(shí)現(xiàn),更多關(guān)于Go數(shù)據(jù)庫(kù)CRUD Mock測(cè)試的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言學(xué)習(xí)技巧之如何合理使用Pool
這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言學(xué)習(xí)技巧之如何合理使用Pool的相關(guān)資料,Pool用于存儲(chǔ)那些被分配了但是沒(méi)有被使用,而未來(lái)可能會(huì)使用的值,以減小垃圾回收的壓力。文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12一文詳解Go語(yǔ)言中的Option設(shè)計(jì)模式
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中Option設(shè)計(jì)模式的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解一下2023-05-05如何使用大學(xué)教育郵箱下載golang等軟件(推薦)
這篇文章主要介紹了如何使用大學(xué)教育郵箱下載goland等軟件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Go+Lua解決Redis秒殺中庫(kù)存與超賣問(wèn)題
本文主要介紹了Go+Lua解決Redis秒殺中庫(kù)存與超賣問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Go Gin實(shí)現(xiàn)文件上傳下載的示例代碼
這篇文章主要介紹了Go Gin實(shí)現(xiàn)文件上傳下載的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Golang基于Vault實(shí)現(xiàn)敏感信息保護(hù)
Vault?是一個(gè)強(qiáng)大的敏感信息管理工具,自帶了多種認(rèn)證引擎和密碼引擎,本文主要探討應(yīng)用程序如何安全地從?Vault?獲取敏感信息,并進(jìn)一步實(shí)現(xiàn)自動(dòng)輪轉(zhuǎn),感興趣的可以了解一下2023-06-06