目錄
- 一、 sqlmock介紹
- 二、安裝
- 三、基本用法
- 四、一個小案例
- 五、Gorm 初始化注意點
一、 sqlmock介紹
sqlmock
是一個用於測試資料庫互動的 Go 模擬庫。它可以模擬 SQL 查詢、插入、更新等操作,並且可以驗證 SQL 語句的執行情況,非常適合用於單元測試中。
二、安裝
go get github.com/DATA-DOG/go-sqlmock
三、基本用法
使用 sqlmock
進行 MySQL 資料庫整合測試的基本步驟如下:
- 建立模擬 DB 連線:
import (
"database/sql"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
func TestMyDBFunction(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error creating mock database: %v", err)
}
defer db.Close()
// 使用 mock 來替代真實的資料庫連線
// db 可以傳遞給被測試的函式進行測試
}
- 設定模擬 SQL 查詢和預期結果:
// 模擬 SQL 查詢並設定預期結果
rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "Alice").AddRow(2, "Bob")
mock.ExpectQuery("SELECT id, name FROM users").WillReturnRows(rows)
- 呼叫被測試的函式,並傳入模擬的資料庫連線:
// 呼叫被測試的函式,傳入模擬的資料庫連線
result := MyDBFunction(db)
// 驗證結果是否符合預期
if result != expected {
t.Errorf("Expected %d, got %d", expected, result)
}
四、一個小案例
這裡我們定義了一個 GORMUserDAO
結構體,它實現了 UserDAO
介面,用於與使用者表進行互動。這個結構體透過 gorm.DB
例項與資料庫進行通訊。
具體來說,GORMUserDAO
提供了 Insert
方法,用於在資料庫中建立新使用者。這個方法接受一個 User
型別的結構體作為引數,該結構體定義了使用者的基本資訊,包括 ID、郵箱、密碼、手機號、生日、暱稱、自我介紹、微信 UnionID 和 OpenID 等欄位。
在 Insert
方法中,首先獲取當前時間戳(以毫秒為單位),並設定使用者的建立時間和更新時間。然後,使用 gorm.DB
的 Create
方法將使用者資訊插入到資料庫中。如果插入操作遇到唯一性約束錯誤(例如郵箱或手機號已存在),方法會返回一個特定的錯誤 ErrUserDuplicate
。
User
結構體定義了資料庫表的結構,其中包含了一些列的定義,如 Email
和 Phone
被設定為唯一索引。此外,還定義了一些列的型別和約束,如 AboutMe
欄位被設定為最大長度為 1024 的字串型別。
提供了一個使用 GORM 進行資料庫操作的 DAO 層,用於處理使用者資料的建立。
// internal/user/dao/user.go
package dao
import (
"context"
"database/sql"
"errors"
"github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"time"
)
var (
ErrUserDuplicate = errors.New("郵箱衝突")
)
type UserDAO interface {
Insert(ctx context.Context, u User) error
}
type GORMUserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) UserDAO {
return &GORMUserDAO{
db: db,
}
}
func (dao *GORMUserDAO) Insert(ctx context.Context, u User) error {
// 存毫秒數
now := time.Now().UnixMilli()
u.Utime = now
u.Ctime = now
err := dao.db.WithContext(ctx).Create(&u).Error
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
const uniqueConflictsErrNo uint16 = 1062
if mysqlErr.Number == uniqueConflictsErrNo {
// 郵箱衝突 or 手機號碼衝突
return ErrUserDuplicate
}
}
return err
}
// User 直接對應資料庫表結構
// 有些人叫做 entity,有些人叫做 model,有些人叫做 PO(persistent object)
type User struct {
Id int64 `gorm:"primaryKey,autoIncrement"`
// 設定為唯一索引
Email sql.NullString `gorm:"unique"`
Password string
//Phone *string
Phone sql.NullString `gorm:"unique"`
Birthday sql.NullInt64
// 暱稱
Nickname sql.NullString
// 自我介紹
AboutMe sql.NullString `gorm:"type=varchar(1024)"`
WechatUnionID sql.NullString
WechatOpenID sql.NullString `gorm:"unique"`
// 建立時間
Ctime int64
// 更新時間
Utime int64
}
接著我們用編寫測試用例
package dao
import (
"context"
"database/sql"
"errors"
"github.com/DATA-DOG/go-sqlmock"
"github.com/go-sql-driver/mysql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gormMysql "gorm.io/driver/mysql"
"gorm.io/gorm"
"testing"
)
func TestGORMUserDAO_Insert(t *testing.T) {
//
testCases := []struct {
name string
// 為什麼不用 ctrl ?
// 因為你這裡是 sqlmock,不是 gomock
mock func(t *testing.T) *sql.DB
ctx context.Context
user User
wantErr error
}{
{
name: "插入成功",
mock: func(t *testing.T) *sql.DB {
mockDB, mock, err := sqlmock.New()
res := sqlmock.NewResult(3, 1)
// 這邊預期的是正規表示式
// 這個寫法的意思就是,只要是 INSERT 到 users 的語句
mock.ExpectExec("INSERT INTO `users` .*").
WillReturnResult(res)
require.NoError(t, err)
return mockDB
},
user: User{
Email: sql.NullString{
String: "123@qq.com",
Valid: true,
},
},
},
{
name: "郵箱衝突",
mock: func(t *testing.T) *sql.DB {
mockDB, mock, err := sqlmock.New()
// 這邊預期的是正規表示式
// 這個寫法的意思就是,只要是 INSERT 到 users 的語句
mock.ExpectExec("INSERT INTO `users` .*").
WillReturnError(&mysql.MySQLError{
Number: 1062,
})
require.NoError(t, err)
return mockDB
},
user: User{},
wantErr: ErrUserDuplicate,
},
{
name: "資料庫錯誤",
mock: func(t *testing.T) *sql.DB {
mockDB, mock, err := sqlmock.New()
// 這邊預期的是正規表示式
// 這個寫法的意思就是,只要是 INSERT 到 users 的語句
mock.ExpectExec("INSERT INTO `users` .*").
WillReturnError(errors.New("資料庫錯誤"))
require.NoError(t, err)
return mockDB
},
user: User{},
wantErr: errors.New("資料庫錯誤"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db, err := gorm.Open(gormMysql.New(gormMysql.Config{
Conn: tc.mock(t),
// SELECT VERSION;
SkipInitializeWithVersion: true,
}), &gorm.Config{
// 你 mock DB 不需要 ping
DisableAutomaticPing: true,
// 這個是什麼呢?
SkipDefaultTransaction: true,
})
d := NewUserDAO(db)
u := tc.user
err = d.Insert(tc.ctx, u)
assert.Equal(t, tc.wantErr, err)
})
}
}
五、Gorm 初始化注意點
這裡執行測試的程式碼也有點與眾不同,在初始化 GORM 的時候需要額外設定三個引數。
SkipInitializeWithVersion
:如果為false
,那麼 GORM 在初始化的時候,會先呼叫show version
。DisableAutomiticPing
:為true
不允許Ping
資料庫。SkipDefaultTransaction
:為false
的時候,即便是一個單一增刪改語句,GORM 也會開啟事務。
這三個選項禁用之後,就可以確保 GORM 不會在初始化的過程中發起額外的呼叫。