golang数据库交互测试模拟SQL驱动插件库go-sqlmock的使用
Golang 数据库交互测试模拟 SQL 驱动插件库 go-sqlmock 的使用
go-sqlmock 是一个实现了 sql/driver 接口的模拟库,它的唯一目的是在测试中模拟任何 SQL 驱动程序行为,而无需真实的数据库连接。它有助于保持正确的 TDD 工作流程。
特性
- 库现已完整且稳定
- 支持并发和多个连接
- 支持 go1.8 的 Context 相关功能模拟和命名 SQL 参数
- 不需要对源代码进行任何修改
- 驱动程序允许模拟任何 SQL 驱动程序方法行为
- 默认情况下具有严格的期望顺序匹配
- 没有第三方依赖
安装
go get github.com/DATA-DOG/go-sqlmock
使用示例
测试更新统计功能
假设我们有以下需要测试的函数:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func recordStats(db *sql.DB, userID, productID int64) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
return
}
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
return
}
return
}
成功测试用例
package main
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
// 成功案例
func TestShouldUpdateStats(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).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// 现在执行我们的方法
if err = recordStats(db, 2, 3); err != nil {
t.Errorf("error was not expected while updating stats: %s", err)
}
// 确保所有期望都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
失败测试用例
// 失败案例
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()
// 现在执行我们的方法
if err = recordStats(db, 2, 3); err == nil {
t.Errorf("was expecting an error, but there was none")
}
// 确保所有期望都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
自定义 SQL 查询匹配
go-sqlmock 提供了 QueryMatcher
接口,可以在调用 sqlmock.New
或 sqlmock.NewWithDSN
时通过选项传递。
默认情况下,go-sqlmock 保持向后兼容性,默认查询匹配器是 sqlmock.QueryMatcherRegexp
,它使用预期的 SQL 字符串作为正则表达式来匹配传入的查询字符串。还有一个相等匹配器:QueryMatcherEqual
,它将执行完全区分大小写的匹配。
自定义 QueryMatcher 的示例:
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
匹配 time.Time 等参数
对于像 time.Time
这样的结构体类型参数,go-sqlmock 提供了 Argument
接口,可以用于更复杂的匹配。以下是时间参数匹配的简单示例:
type AnyTime struct{}
// Match 满足 sqlmock.Argument 接口
func (a AnyTime) Match(v driver.Value) bool {
_, ok := v.(time.Time)
return ok
}
func TestAnyTimeArgument(t *testing.T) {
t.Parallel()
db, mock, err := sqlmock.New()
if err != nil {
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
mock.ExpectExec("INSERT INTO users").
WithArgs("john", AnyTime{}).
WillReturnResult(sqlmock.NewResult(1, 1))
_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
if err != nil {
t.Errorf("error '%s' was not expected, while inserting a row", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
运行测试
go test -race
go-sqlmock 是一个强大的工具,可以帮助你在不依赖真实数据库的情况下测试数据库交互代码,确保你的代码在各种情况下都能正确处理数据库操作。
更多关于golang数据库交互测试模拟SQL驱动插件库go-sqlmock的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang数据库交互测试模拟SQL驱动插件库go-sqlmock的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
使用go-sqlmock进行Golang数据库交互测试
go-sqlmock是一个用于模拟SQL驱动的库,非常适合在不需要实际数据库连接的情况下测试数据库交互代码。下面我将详细介绍如何使用它。
安装go-sqlmock
首先安装go-sqlmock:
go get github.com/DATA-DOG/go-sqlmock
基本使用方法
1. 创建模拟数据库连接
import (
"database/sql"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
func TestUserQuery(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()
// 在这里编写测试代码...
}
2. 设置预期查询
// 设置预期查询
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "John Doe", "john@example.com").
AddRow(2, "Jane Doe", "jane@example.com")
mock.ExpectQuery("^SELECT id, name, email FROM users WHERE id = \\?$").
WithArgs(1).
WillReturnRows(rows)
3. 执行查询并验证结果
// 执行查询
user, err := GetUser(db, 1)
if err != nil {
t.Errorf("error was not expected while getting user: %s", err)
}
// 验证结果
if user.Name != "John Doe" {
t.Errorf("expected name to be 'John Doe', got '%s'", user.Name)
}
// 确保所有期望都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
完整示例
下面是一个完整的测试示例,包含查询和插入操作:
package main
import (
"database/sql"
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
type User struct {
ID int
Name string
Email string
}
func GetUser(db *sql.DB, id int) (*User, error) {
var user User
err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
func InsertUser(db *sql.DB, user *User) error {
_, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
return err
}
func TestUserOperations(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()
// 测试查询
t.Run("Test GetUser", func(t *testing.T) {
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "John Doe", "john@example.com")
mock.ExpectQuery("^SELECT id, name, email FROM users WHERE id = \\?$").
WithArgs(1).
WillReturnRows(rows)
user, err := GetUser(db, 1)
if err != nil {
t.Errorf("error was not expected while getting user: %s", err)
}
if user.Name != "John Doe" {
t.Errorf("expected name to be 'John Doe', got '%s'", user.Name)
}
})
// 测试插入
t.Run("Test InsertUser", func(t *testing.T) {
mock.ExpectExec("^INSERT INTO users").
WithArgs("Alice", "alice@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
err := InsertUser(db, &User{Name: "Alice", Email: "alice@example.com"})
if err != nil {
t.Errorf("error was not expected while inserting user: %s", err)
}
})
// 确保所有期望都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
高级用法
1. 模拟事务
func TestTransaction(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 users").WithArgs("new@email.com", 1).
WillReturnResult(sqlmock.NewResult(0, 1))
// 提交事务
mock.ExpectCommit()
// 执行事务代码
err = UpdateUserEmail(db, 1, "new@email.com")
if err != nil {
t.Errorf("error was not expected while updating user email: %s", err)
}
}
2. 模拟错误
func TestQueryError(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.ExpectQuery("SELECT").WillReturnError(fmt.Errorf("some error"))
_, err = GetUser(db, 1)
if err == nil {
t.Error("expected error but got none")
}
}
最佳实践
- 每个测试用例后检查ExpectationsWereMet:确保所有预期的SQL调用都发生了
- 使用正则表达式匹配SQL:更灵活地匹配SQL语句
- 为每个测试用例创建新的mock:避免测试间的相互影响
- 模拟真实场景:返回的行和结果应该尽可能接近真实情况
- 测试错误场景:不要只测试成功路径,也要测试错误处理
go-sqlmock是一个强大的工具,可以让你在不依赖真实数据库的情况下全面测试数据库交互代码。通过合理设置预期和验证结果,你可以确保你的数据库访问层在各种情况下都能正确工作。