在数据库应用开发过程中,会在数据库上执行各种 SQL 语句。
在做单元测试的时候,一般不会与实际数据库交互,这时就需要mock 数据库操作。
即
在不建立真实连接的情况下,模拟 sql driver 中的各种操作。
本文介绍golang中 mock sql 操作的 库 go-sqlmock。
go get github.com/DATA-DOG/go-sqlmock
介绍下github上的例子:
代码中,执行有两个操作 update和insert 组成的事务。
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)
}
}
单元测试代码
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)
}
}
第一个测试用例中,模拟事务正常提交。
首先模拟连接。
接着,模拟update 和insert 。
其中,
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
表示执行update时返回结果1 1
,一个是lastInsertID
,一个是rowsAffected
。
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
表示执行insert时,使用2 3
作为参数,并返回结果1 1
。
mock.ExpectBegin()
和 mock.ExpectCommit()
表示mock 事务的开始和结束。
第二个测试用例,与第一个不同的地方是,执行insert时报错。
mock.ExpectExec("INSERT INTO product_viewers"). WithArgs(2, 3). WillReturnError(fmt.Errorf("some error"))
表示执行insert时,使用2 3
作为参数,并返回错误。