Golang的Sql驱动模拟
sqlmock是一个实现sql/driver的mock库。它有一个唯一的目的--在测试中模拟任何sql驱动的行为,而不需要真正的数据库连接。它有助于保持正确的TDD工作流程:
- 这个库现在是完整和稳定的。(你可能不会因为这个原因而发现新的变化)
- 支持并发和多连接。
- 支持go1.8Context相关功能mocking和Named sql参数。
- 不需要对你的源代码进行任何修改。
- 该驱动允许嘲弄任何sql驱动方法的行为。
- 有严格的默认期望顺序匹配。
- 没有第三方的依赖性。
注意:在v1.2.0版本中,sqlmock.Rows已经从接口变成了结构,如果你使用任何对该接口的类型引用,你将需要把它切换到一个指针结构类型。另外,sqlmock.Rows被用来实现driver.Rows接口,这对于嘲讽来说是不需要的,也是没有用的,所以被移除。希望它不会引起问题。
寻找维护者
我没有太多的空闲时间来维护这个库,我愿意把库的所有权转让给有动力维护它的人或组织。如果你有兴趣的话,可以打开对话。见#230。
安装
go get github.com/DATA-DOG/go-sqlmock
文档和例子
访问godoc获取一般的例子和公共api参考。参见**.travis.yml以了解支持的go**版本。不同的用例,是用一个真实的数据库进行功能测试--go-txdb所有与数据库相关的操作都被隔离在一个事务中,所以数据库可以保持在相同的状态。
参见实现示例:
你可能想测试的东西,假设你使用go-mysql-driver
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
}
func main() {
// @NOTE: the real connection is not required for tests
db, err := sql.Open("mysql", "root@/blog")
if err != nil {
panic(err)
}
defer db.Close()
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
panic(err)
}
}
用sqlmock进行测试
package main
import (
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
// a successful case
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()
// now we execute our method
if err = recordStats(db, 2, 3); err != nil {
t.Errorf("error was not expected while updating stats: %s", err)
}
// we make sure that all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
// a failing test case
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)
}
}
自定义SQL查询匹配
有很多用户要求关于SQL查询字符串验证或不同的匹配选项。我们现在已经实现了QueryMatcher 接口,在调用sqlmock.New 或sqlmock.NewWithDSN 时可以通过一个选项来传递。
现在,这允许包括一些库,这将允许例如解析和验证mysql SQL AST。并创建一个自定义的QueryMatcher,以便以复杂的方式验证SQL。
默认情况下,sqlmock是向后兼容的,默认的查询匹配器是sqlmock.QueryMatcherRegexp ,它使用预期的SQL字符串作为一个正则表达式来匹配传入的查询字符串。有一个平等匹配器:QueryMatcherEqual ,它将做一个完全的大小写敏感的匹配。
为了定制QueryMatcher,请使用以下方法:
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
sqlmock不会提供一个标准的sql解析匹配器,因为各种驱动可能不遵循相同的SQL标准。
匹配参数如time.Time
可能有一些参数是struct 类型的,不能很容易地通过值进行比较,比如time.Time 。在这种情况下,sqlmock提供了一个Argument接口,可以用于更复杂的匹配。下面是一个简单的时间参数匹配的例子。
type AnyTime struct{}
// Match satisfies sqlmock.Argument interface
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)
}
}
它只断言参数的类型是time.Time 。
运行测试
go test -race
更改日志
- 2019-04-06- 增加了模拟sql MetaData请求的功能
- 2019-02-13- 增加了
go.mod删除了使用gopkg.in的参考和建议。 - 2018-12-11- 在模拟预期的查询时,增加了期望行被关闭的功能。
- 2018-12-11- 引入了一个提供QueryMatcher的选项,以便自定义SQL查询匹配。
- 2017-09-01- 现在可以使用ExpectedPrepare.WillBeClosed来期望准备好的语句将被关闭。
- 2017-02-09- 实现了对go1.8功能的支持。Rows接口被改为结构,但包含所有的方法,如以前一样,应该保持向后兼容。ExpectedQuery.WillReturnRows现在可以接受多个行集。
- 2016-11-02-
db.Prepare()没有验证预期的准备SQL查询。即使Exec或Query没有在该准备语句上执行,它也应该被验证。 - 2016-02-23- 增加了**sqlmock.AnyArg()**函数以提供任何种类的参数匹配器。
- 2016-02-23- 像自然驱动一样将预期参数转换为驱动.Value,这个变化可能会影响time.Time的比较,会更加严格。见问题。
- 2015-08-27-v1api变更,支持并发,所有已知问题都已修复。
- 2014-08-16在比较查询参数时反映类型不匹配,而不是恐慌- 现在返回错误
- 2014-08-14增加了sqlmock.NewErrorResult,它提供了一个选项来返回带有接口方法错误的driver.Result,见问题
- 2014-05-29通过提供sqlmock.Argument接口,允许以更复杂的方式匹配参数。
- 2014-04-21引入sqlmock.New(),为测试打开一个模拟的数据库连接。这个方法调用sql.DB.Ping来确保连接是开放的,见问题。这种方式在关闭时肯定会断言是否满足所有期望,即使数据库根本没有被触发。旧的方法仍然可用,但建议在用db.close断言之前手动调用db.Ping。
- 2014-02-14RowsFromCSVString现在是Rows接口的一部分,名为FromCSVString。它的改变是为了允许更多的方式来构建行,并在未来轻松地扩展这个API。参见问题1 RowsFromCSVString已被废弃,并将在未来被移除。
贡献
请自由地打开一个拉动请求。请注意,如果你想对公共部分(导出的方法或类型)进行扩展--请先开一个问题,以讨论这些变化是否可以被接受。所有向后不兼容的修改都将被谨慎对待。