Golang的Sql驱动模拟

629 阅读6分钟

Build StatusGoDocGo Report Cardcodecov.io

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.Newsqlmock.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已被废弃,并将在未来被移除。

贡献

请自由地打开一个拉动请求。请注意,如果你想对公共部分(导出的方法或类型)进行扩展--请先开一个问题,以讨论这些变化是否可以被接受。所有向后不兼容的修改都将被谨慎对待。

许可证

三个条款的BSD许可证