在專案裡怎麼給 GORM 做單元測試
前言
真實的開發場景下我們的專案一般都會使用 ORM ,而不是原生的database/sql來完成資料庫操作。在很多使用ORM工具的場景下,也可以使用go-sqlmock庫 Mock資料庫操作進行測試,今天這篇內容我就以 GORM 為例,講解怎麼給專案中的 ORM 資料庫操作做單元測試。
專案準備
為了場景足夠真實,我用 2020 年我更新的 「Go Web 程式設計入門」專案中的例子給大家演示怎麼為使用了 GORM 的 DAO 層邏輯做 Mock 測試。
這裡使用的GORM版本為 1.x,有可能在2.x版本下不相容。
在這個例子中我們有一個與 users 表:
type User struct { Id int64 `gorm:"column:id;primary_key"` UserName string `gorm:"column:username"` Secret string `gorm:"column:secret;type:varchar(1000)"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (m *User) TableName() string { return "users" }
以及幾個使用 User 的 DAO 函式:
var _DB *gorm.DB func DB() *gorm.DB { return _DB } func init() { //這裡邏輯省略,就是初始化 GORM 的DB物件, // 設定連線資料庫的配置 // 真實程式碼可以公眾號回覆【gohttp15】獲得 _DB = initDB() } func CreateUser(user *table.User) (err error) { err = DB().Create(user).Error return } func GetUserByNameAndPassword(name, password string) (user *table.User, err error) { user = new(table.User) err = DB().Where("username = ? AND secret = ?", name, password). First(&user).Error return } func UpdateUserNameById(userName string, userId int64) (err error) { user := new(table.User) updated := map[string]interface{}{ "username": userName, } err = DB().Model(user).Where("id = ?", userId).Updates(updated).Error return }
接下來我們就用 go-sqlmock 工具給這幾個 DAO 函式做一下 Mock 測試。
初始化測試工作
首先我們需要做一下測試的初始化工作,主要是設定Mock的DB連線,因為要給三個方法做Mock測試,最簡單的辦法是在三個方法裡每次都初始化一遍 Mock 的 DB 連線,不過這麼做實在是顯得有點蠢,這裡給大家再介紹一個小技巧。
Go 的測試支援在包內優先執行一個 TestMain(m *testing.M) 函式,可以在這裡為 package 下所有測試做一些初始化的工作。
下面是我們為本次測試做的初始化工作。
// 給公眾號「網管叨bi叨」發私信 // gohttp15 獲得原始碼 var ( mock sqlmock.Sqlmock err error db *sql.DB ) // TestMain是在當前package下,最先執行的一個函式,常用於初始化 func TestMain(m *testing.M) { //把匹配器設定成相等匹配器,不設定預設使用正則匹配 db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) if err != nil { panic(err) } _DB, err = gorm.Open("mysql", db) // m.Run 是呼叫包下面各個Test函式的入口 os.Exit(m.Run()) }
- 在這個初始化函式裡我們建立一個 sqlmock 的資料庫連線 db 和 mock物件,mock物件管理 db 預期要執行的SQL。
- 讓sqlmock 使用 QueryMatcherEqual 匹配器,該匹配器把mock.ExpectQuery 和 mock.ExpectExec 的引數作為預期要執行的SQL語句跟實際要執行的SQL進行相等比較。
- m.Run 是呼叫包下面各個Test函式的入口。
準備工作做好了,下面正式對 DAO 操作進行Mock測試。
對Create進行Mock測試
首先對 GORM 的Create 方法進行Mock測試。
// 給公眾號「網管叨bi叨」發私信 // gohttp15 獲得原始碼 func TestCreateUserMock(t *testing.T) { user := &table.User{ UserName: "Kevin", Secret: "123456", CreatedAt: time.Now(), UpdatedAt: time.Now(), } mock.ExpectBegin() mock.ExpectExec("INSERT INTO `users` (`username`,`secret`,`created_at`,`updated_at`) VALUES (?,?,?,?)"). WithArgs(user.UserName, user.Secret, user.CreatedAt, user.UpdatedAt). WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() err := CreateUser(user) assert.Nil(t, err) }
因為 sqlmock 使用的是 QueryMatcherEqual 匹配器,所以,預期會執行的 SQL 語句必須精確匹配要執行的SQL(包括符號和空格)。
這個SQL怎麼獲取呢?其實我們先隨便寫一個SQL,執行一次測試,在報錯資訊裡就會告知CreateUser操作在寫表時 GORM 真正要執行的 SQL 啦。還有一種方法是通過GORM提供的Debug()方法獲取到。
比如執行一下下面這個設定了Debug()的建立使用者操作,GORM就會打印出執行的語句。
func CreateUser(user *table.User) (err error) { // 打印出要執行的SQL語句 ,記得改回去 err = DB().Debug().Create(user).Error // err = DB().Create(user).Error return }
我們執行下這個測試
go test -v -run TestCreateUserMock -------- === RUN TestCreateUserMock --- PASS: TestCreateUserMock (0.00s) PASS ok golang-unit-test-demo/sqlmock_gorm_demo 0.301s
可以看到,測試函式執行成功,我們還可以故意把SQL改錯,做一下反向測試。這個就留給你們自己練習啦,結合上表格測試分別做一下正向和反向單元測試。
Get 操作的Mock測試
GORM 的查詢操作的Mock測試跟Create類似。
// 給公眾號「網管叨bi叨」發私信 // gohttp15 獲得原始碼 func TestGetUserByNameAndPasswordMock(t *testing.T) { user := &User{ Id: 1, UserName: "Kevin", Secret: "123456", CreatedAt: time.Now(), UpdatedAt: time.Now(), } mock.ExpectQuery("SELECT * FROM `users` WHERE (username = ? AND secret = ?) "+ "ORDER BY `users`.`id` ASC LIMIT 1"). WithArgs(user.UserName, user.Secret). WillReturnRows( // 這裡要跟結果集包含的列匹配,因為查詢是 SELECT * 所以表的欄位都要列出來 sqlmock.NewRows([]string{"id", "username", "secret", "created_at", "updated_at"}). AddRow(1, user.UserName, user.Secret, user.CreatedAt, user.UpdatedAt)) res, err := GetUserByNameAndPassword(user.UserName, user.Secret) assert.Nil(t, err) assert.Equal(t, user, res) }
這裡就不在文章裡執行演示啦,有興趣的自己把程式碼拿下來試一下。
Update 操作的Mock測試
GORM的Update操作我沒有測試成功,我這裡發出來,大家看一下原因。
func TestUpdateUserNameByIdMock(t *testing.T) { newName := "Kev" var userId int64 = 1 mock.ExpectBegin() mock.ExpectExec("UPDATE `users` SET `updated_at` = ?, `username` = ? WHERE (id = ?)"). WithArgs(time.Now(), newName, userId). WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() err := UpdateUserNameById(newName, userId) assert.Nil(t, err) }
執行測試後,會有下面的報錯資訊:
ExecQuery 'UPDATE `users` SET `updated_at` = ?, `username` = ? WHERE (id = ?)', arguments do not match: argument 0 expected [time.Time - 2022-05-08 18:13:08.23323 +0800 CST m=+0.003082084] does not match actual [time.Time - 2022-05-08 18:13:08.234134 +0800 CST m=+0.003986334]
GORM 在UPDATE 的時候會自動更新updated_at 欄位為當前時間,與這裡withArgs傳遞的 time.Now() 引數不一致(毫秒級的差距也不行)。
目前沒有辦法 Mock 測試 GORM 的UPDATE,除非用 GORM 的 Exec 方法直接執行要更新的SQL,不過那就失去使用ORM的意義了,所以這個先跳過,如果有這方面經驗的大佬,可以在留言裡指導一下。
總結
這篇內容我們把ORM的 Mock 測試做了一個講解,這個也是我在學習 Go 單元測試時自己的思考,希望學習到的這些技能能在專案中真實用到。
因為文章中的示例,是以我之前的Go Web 程式設計教程裡的專案裡做的測試,原始碼我也打包更新到了Go Web 程式設計的專案中啦,公眾號私信 gohttp15 就能獲得。
如果你覺得有用,可以點贊、在看、分享給更多人,謝謝各位的支援,後面會與時俱進再搞一篇 Go 1.18 Fuzing 測試的使用介紹。
- 用於資料管理的多雲策略有哪些?
- Redis 的記憶體淘汰策略和過期刪除策略,你別再搞混了!
- 美國大廠薪水第二彈!Twitter底薪六位數,Uber虧損仍開出20多萬美元
- 9.6K Star!可擴充套件的富文字編輯框架!
- 自動駕駛系統中的邊緣計算技術
- 大資料專案可能出錯的五種方式
- 一篇學會樹的子結構
- 假期來啦!技術人如何用 Python 實現景區安防系統
- 從20s優化到500ms,我用了這三招
- 分庫分表實戰:最初的我們—瞭解一下單庫外賣訂單系統
- 手把手教你用裝飾器擴充套件 Python 計時器
- 一個99%的人都說不清楚知識點—Spring 事務傳播行為
- RP原型資源分享-購物類App
- 使用 rustup 管理你的 Rust 工具鏈
- 實現各種效果和功能的按鈕,讀這篇文章就夠了
- 七個好用常見的大資料分析模型
- MITRE組織公佈了2022年CWE最危險的25個軟體弱點
- Java反序列化基礎篇-JDK動態代理
- iOS 16 即將讓你的 iPhone 擺脫那些特別讓人煩的驗證碼和垃圾廣告
- 刷演算法題常用的 JS 基礎掃盲