mirror of
https://github.com/zhashkevych/go-sqlxmock.git
synced 2024-11-24 08:12:13 +02:00
32a1b9d93f
* dcca987 add an old example and a basic one
445 lines
12 KiB
Go
445 lines
12 KiB
Go
/*
|
|
Package sqlmock provides sql driver connection, which allows to test database
|
|
interactions by expected calls and simulate their results or errors.
|
|
|
|
It does not require any modifications to your source code in order to test
|
|
and mock database operations. It does not even require a real database in order
|
|
to test your application.
|
|
|
|
The driver allows to mock any sql driver method behavior. Concurrent actions
|
|
are also supported.
|
|
*/
|
|
package sqlmock
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
)
|
|
|
|
// Sqlmock type satisfies required sql.driver interfaces
|
|
// to simulate actual database and also serves to
|
|
// create expectations for any kind of database action
|
|
// in order to mock and test real database behavior.
|
|
type Sqlmock struct {
|
|
|
|
// MatchExpectationsInOrder gives an option whether to match all
|
|
// expectations in the order they were set or not.
|
|
//
|
|
// By default it is set to - true. But if you use goroutines
|
|
// to parallelize your query executation, that option may
|
|
// be handy.
|
|
MatchExpectationsInOrder bool
|
|
|
|
dsn string
|
|
opened int
|
|
drv *mockDriver
|
|
|
|
expected []expectation
|
|
}
|
|
|
|
// ExpectClose queues an expectation for this database
|
|
// action to be triggered. the *ExpectedClose allows
|
|
// to mock database response
|
|
func (c *Sqlmock) ExpectClose() *ExpectedClose {
|
|
e := &ExpectedClose{}
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// Close a mock database driver connection. It may or may not
|
|
// be called depending on the sircumstances, but if it is called
|
|
// there must be an *ExpectedClose expectation satisfied.
|
|
// meets http://golang.org/pkg/database/sql/driver/#Conn interface
|
|
func (c *Sqlmock) Close() error {
|
|
c.drv.Lock()
|
|
defer c.drv.Unlock()
|
|
|
|
c.opened--
|
|
if c.opened == 0 {
|
|
delete(c.drv.conns, c.dsn)
|
|
}
|
|
|
|
var expected *ExpectedClose
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if expected, ok = next.(*ExpectedClose); ok {
|
|
break
|
|
}
|
|
|
|
next.Unlock()
|
|
if c.MatchExpectationsInOrder {
|
|
return fmt.Errorf("call to database Close, was not expected, next expectation is: %s", next)
|
|
}
|
|
}
|
|
|
|
if expected == nil {
|
|
msg := "call to database Close was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return fmt.Errorf(msg)
|
|
}
|
|
|
|
expected.triggered = true
|
|
expected.Unlock()
|
|
return expected.err
|
|
}
|
|
|
|
// ExpectationsWereMet checks whether all queued expectations
|
|
// were met in order. If any of them was not met - an error is returned.
|
|
func (c *Sqlmock) ExpectationsWereMet() error {
|
|
for _, e := range c.expected {
|
|
if !e.fulfilled() {
|
|
return fmt.Errorf("there is a remaining expectation which was not matched: %s", e)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Begin meets http://golang.org/pkg/database/sql/driver/#Conn interface
|
|
func (c *Sqlmock) Begin() (driver.Tx, error) {
|
|
var expected *ExpectedBegin
|
|
var ok bool
|
|
var fulfilled int
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if expected, ok = next.(*ExpectedBegin); ok {
|
|
break
|
|
}
|
|
|
|
next.Unlock()
|
|
if c.MatchExpectationsInOrder {
|
|
return nil, fmt.Errorf("call to database transaction Begin, was not expected, next expectation is: %s", next)
|
|
}
|
|
}
|
|
if expected == nil {
|
|
msg := "call to database transaction Begin was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return nil, fmt.Errorf(msg)
|
|
}
|
|
|
|
expected.triggered = true
|
|
expected.Unlock()
|
|
return c, expected.err
|
|
}
|
|
|
|
// ExpectBegin expects *sql.DB.Begin to be called.
|
|
// the *ExpectedBegin allows to mock database response
|
|
func (c *Sqlmock) ExpectBegin() *ExpectedBegin {
|
|
e := &ExpectedBegin{}
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// Exec meets http://golang.org/pkg/database/sql/driver/#Execer
|
|
func (c *Sqlmock) Exec(query string, args []driver.Value) (res driver.Result, err error) {
|
|
query = stripQuery(query)
|
|
var expected *ExpectedExec
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if c.MatchExpectationsInOrder {
|
|
if expected, ok = next.(*ExpectedExec); ok {
|
|
break
|
|
}
|
|
next.Unlock()
|
|
return nil, fmt.Errorf("call to exec query '%s' with args %+v, was not expected, next expectation is: %s", query, args, next)
|
|
}
|
|
if exec, ok := next.(*ExpectedExec); ok {
|
|
if exec.attemptMatch(query, args) {
|
|
expected = exec
|
|
break
|
|
}
|
|
}
|
|
next.Unlock()
|
|
}
|
|
if expected == nil {
|
|
msg := "call to exec '%s' query with args %+v was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return nil, fmt.Errorf(msg, query, args)
|
|
}
|
|
|
|
defer expected.Unlock()
|
|
expected.triggered = true
|
|
// converts panic to error in case of reflect value type mismatch
|
|
defer func(errp *error, exp *ExpectedExec, q string, a []driver.Value) {
|
|
if e := recover(); e != nil {
|
|
if se, ok := e.(*reflect.ValueError); ok { // catch reflect error, failed type conversion
|
|
msg := "exec query \"%s\", args \"%+v\" failed to match with error \"%s\" expectation: %s"
|
|
*errp = fmt.Errorf(msg, q, a, se, exp)
|
|
} else {
|
|
panic(e) // overwise if unknown error panic
|
|
}
|
|
}
|
|
}(&err, expected, query, args)
|
|
|
|
if !expected.queryMatches(query) {
|
|
return nil, fmt.Errorf("exec query '%s', does not match regex '%s'", query, expected.sqlRegex.String())
|
|
}
|
|
|
|
if !expected.argsMatches(args) {
|
|
return nil, fmt.Errorf("exec query '%s', args %+v does not match expected %+v", query, args, expected.args)
|
|
}
|
|
|
|
if expected.err != nil {
|
|
return nil, expected.err // mocked to return error
|
|
}
|
|
|
|
if expected.result == nil {
|
|
return nil, fmt.Errorf("exec query '%s' with args %+v, must return a database/sql/driver.result, but it was not set for expectation %T as %+v", query, args, expected, expected)
|
|
}
|
|
return expected.result, err
|
|
}
|
|
|
|
// ExpectExec expects Exec() to be called with sql query
|
|
// which match sqlRegexStr given regexp.
|
|
// the *ExpectedExec allows to mock database response
|
|
func (c *Sqlmock) ExpectExec(sqlRegexStr string) *ExpectedExec {
|
|
e := &ExpectedExec{}
|
|
e.sqlRegex = regexp.MustCompile(sqlRegexStr)
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// Prepare meets http://golang.org/pkg/database/sql/driver/#Conn interface
|
|
func (c *Sqlmock) Prepare(query string) (driver.Stmt, error) {
|
|
var expected *ExpectedPrepare
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if expected, ok = next.(*ExpectedPrepare); ok {
|
|
break
|
|
}
|
|
|
|
next.Unlock()
|
|
if c.MatchExpectationsInOrder {
|
|
return nil, fmt.Errorf("call to Prepare stetement with query '%s', was not expected, next expectation is: %s", query, next)
|
|
}
|
|
}
|
|
|
|
query = stripQuery(query)
|
|
if expected == nil {
|
|
msg := "call to Prepare '%s' query was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return nil, fmt.Errorf(msg, query)
|
|
}
|
|
|
|
expected.triggered = true
|
|
expected.Unlock()
|
|
return &statement{c, query, expected.closeErr}, expected.err
|
|
}
|
|
|
|
// ExpectPrepare expects Prepare() to be called with sql query
|
|
// which match sqlRegexStr given regexp.
|
|
// the *ExpectedPrepare allows to mock database response.
|
|
// Note that you may expect Query() or Exec() on the *ExpectedPrepare
|
|
// statement to prevent repeating sqlRegexStr
|
|
func (c *Sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare {
|
|
e := &ExpectedPrepare{sqlRegex: regexp.MustCompile(sqlRegexStr), mock: c}
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// Query meets http://golang.org/pkg/database/sql/driver/#Queryer
|
|
func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err error) {
|
|
query = stripQuery(query)
|
|
var expected *ExpectedQuery
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if c.MatchExpectationsInOrder {
|
|
if expected, ok = next.(*ExpectedQuery); ok {
|
|
break
|
|
}
|
|
next.Unlock()
|
|
return nil, fmt.Errorf("call to query '%s' with args %+v, was not expected, next expectation is: %s", query, args, next)
|
|
}
|
|
if qr, ok := next.(*ExpectedQuery); ok {
|
|
if qr.attemptMatch(query, args) {
|
|
expected = qr
|
|
break
|
|
}
|
|
}
|
|
next.Unlock()
|
|
}
|
|
|
|
if expected == nil {
|
|
msg := "call to query '%s' with args %+v was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return nil, fmt.Errorf(msg, query, args)
|
|
}
|
|
|
|
defer expected.Unlock()
|
|
expected.triggered = true
|
|
// converts panic to error in case of reflect value type mismatch
|
|
defer func(errp *error, exp *ExpectedQuery, q string, a []driver.Value) {
|
|
if e := recover(); e != nil {
|
|
if se, ok := e.(*reflect.ValueError); ok { // catch reflect error, failed type conversion
|
|
msg := "query \"%s\", args \"%+v\" failed to match with error \"%s\" expectation: %s"
|
|
*errp = fmt.Errorf(msg, q, a, se, exp)
|
|
} else {
|
|
panic(e) // overwise if unknown error panic
|
|
}
|
|
}
|
|
}(&err, expected, query, args)
|
|
|
|
if !expected.queryMatches(query) {
|
|
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
|
|
}
|
|
|
|
if !expected.argsMatches(args) {
|
|
return nil, fmt.Errorf("query '%s', args %+v does not match expected %+v", query, args, expected.args)
|
|
}
|
|
|
|
if expected.err != nil {
|
|
return nil, expected.err // mocked to return error
|
|
}
|
|
|
|
if expected.rows == nil {
|
|
return nil, fmt.Errorf("query '%s' with args %+v, must return a database/sql/driver.rows, but it was not set for expectation %T as %+v", query, args, expected, expected)
|
|
}
|
|
|
|
return expected.rows, err
|
|
}
|
|
|
|
// ExpectQuery expects Query() or QueryRow() to be called with sql query
|
|
// which match sqlRegexStr given regexp.
|
|
// the *ExpectedQuery allows to mock database response.
|
|
func (c *Sqlmock) ExpectQuery(sqlRegexStr string) *ExpectedQuery {
|
|
e := &ExpectedQuery{}
|
|
e.sqlRegex = regexp.MustCompile(sqlRegexStr)
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// ExpectCommit expects *sql.Tx.Commit to be called.
|
|
// the *ExpectedCommit allows to mock database response
|
|
func (c *Sqlmock) ExpectCommit() *ExpectedCommit {
|
|
e := &ExpectedCommit{}
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// ExpectRollback expects *sql.Tx.Rollback to be called.
|
|
// the *ExpectedRollback allows to mock database response
|
|
func (c *Sqlmock) ExpectRollback() *ExpectedRollback {
|
|
e := &ExpectedRollback{}
|
|
c.expected = append(c.expected, e)
|
|
return e
|
|
}
|
|
|
|
// Commit meets http://golang.org/pkg/database/sql/driver/#Tx
|
|
func (c *Sqlmock) Commit() error {
|
|
var expected *ExpectedCommit
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if expected, ok = next.(*ExpectedCommit); ok {
|
|
break
|
|
}
|
|
|
|
next.Unlock()
|
|
if c.MatchExpectationsInOrder {
|
|
return fmt.Errorf("call to commit transaction, was not expected, next expectation is: %s", next)
|
|
}
|
|
}
|
|
if expected == nil {
|
|
msg := "call to commit transaction was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return fmt.Errorf(msg)
|
|
}
|
|
|
|
expected.triggered = true
|
|
expected.Unlock()
|
|
return expected.err
|
|
}
|
|
|
|
// Rollback meets http://golang.org/pkg/database/sql/driver/#Tx
|
|
func (c *Sqlmock) Rollback() error {
|
|
var expected *ExpectedRollback
|
|
var fulfilled int
|
|
var ok bool
|
|
for _, next := range c.expected {
|
|
next.Lock()
|
|
if next.fulfilled() {
|
|
next.Unlock()
|
|
fulfilled++
|
|
continue
|
|
}
|
|
|
|
if expected, ok = next.(*ExpectedRollback); ok {
|
|
break
|
|
}
|
|
|
|
next.Unlock()
|
|
if c.MatchExpectationsInOrder {
|
|
return fmt.Errorf("call to rollback transaction, was not expected, next expectation is: %s", next)
|
|
}
|
|
}
|
|
if expected == nil {
|
|
msg := "call to rollback transaction was not expected"
|
|
if fulfilled == len(c.expected) {
|
|
msg = "all expectations were already fulfilled, " + msg
|
|
}
|
|
return fmt.Errorf(msg)
|
|
}
|
|
|
|
expected.triggered = true
|
|
expected.Unlock()
|
|
return expected.err
|
|
}
|