1
0
mirror of https://github.com/DATA-DOG/go-sqlmock.git synced 2025-06-06 23:26:14 +02:00

implements named arguments support and adds delay expectation for context deadline simulation

This commit is contained in:
gedi 2017-02-06 14:38:57 +02:00
parent 55ecc5a333
commit d11f623794
4 changed files with 191 additions and 24 deletions

View File

@ -7,6 +7,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"time"
) )
// an expectation interface // an expectation interface
@ -54,6 +55,7 @@ func (e *ExpectedClose) String() string {
// returned by *Sqlmock.ExpectBegin. // returned by *Sqlmock.ExpectBegin.
type ExpectedBegin struct { type ExpectedBegin struct {
commonExpectation commonExpectation
delay time.Duration
} }
// WillReturnError allows to set an error for *sql.DB.Begin action // WillReturnError allows to set an error for *sql.DB.Begin action
@ -71,6 +73,13 @@ func (e *ExpectedBegin) String() string {
return msg return msg
} }
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedBegin) WillDelayFor(duration time.Duration) *ExpectedBegin {
e.delay = duration
return e
}
// ExpectedCommit is used to manage *sql.Tx.Commit expectation // ExpectedCommit is used to manage *sql.Tx.Commit expectation
// returned by *Sqlmock.ExpectCommit. // returned by *Sqlmock.ExpectCommit.
type ExpectedCommit struct { type ExpectedCommit struct {
@ -118,7 +127,8 @@ func (e *ExpectedRollback) String() string {
// Returned by *Sqlmock.ExpectQuery. // Returned by *Sqlmock.ExpectQuery.
type ExpectedQuery struct { type ExpectedQuery struct {
queryBasedExpectation queryBasedExpectation
rows driver.Rows rows driver.Rows
delay time.Duration
} }
// WithArgs will match given expected args to actual database query arguments. // WithArgs will match given expected args to actual database query arguments.
@ -142,6 +152,13 @@ func (e *ExpectedQuery) WillReturnRows(rows driver.Rows) *ExpectedQuery {
return e return e
} }
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedQuery) WillDelayFor(duration time.Duration) *ExpectedQuery {
e.delay = duration
return e
}
// String returns string representation // String returns string representation
func (e *ExpectedQuery) String() string { func (e *ExpectedQuery) String() string {
msg := "ExpectedQuery => expecting Query or QueryRow which:" msg := "ExpectedQuery => expecting Query or QueryRow which:"
@ -178,6 +195,7 @@ func (e *ExpectedQuery) String() string {
type ExpectedExec struct { type ExpectedExec struct {
queryBasedExpectation queryBasedExpectation
result driver.Result result driver.Result
delay time.Duration
} }
// WithArgs will match given expected args to actual database exec operation arguments. // WithArgs will match given expected args to actual database exec operation arguments.
@ -194,6 +212,13 @@ func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec {
return e return e
} }
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedExec) WillDelayFor(duration time.Duration) *ExpectedExec {
e.delay = duration
return e
}
// String returns string representation // String returns string representation
func (e *ExpectedExec) String() string { func (e *ExpectedExec) String() string {
msg := "ExpectedExec => expecting Exec which:" msg := "ExpectedExec => expecting Exec which:"
@ -244,6 +269,7 @@ type ExpectedPrepare struct {
sqlRegex *regexp.Regexp sqlRegex *regexp.Regexp
statement driver.Stmt statement driver.Stmt
closeErr error closeErr error
delay time.Duration
} }
// WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action. // WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action.
@ -258,6 +284,13 @@ func (e *ExpectedPrepare) WillReturnCloseError(err error) *ExpectedPrepare {
return e return e
} }
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedPrepare) WillDelayFor(duration time.Duration) *ExpectedPrepare {
e.delay = duration
return e
}
// ExpectQuery allows to expect Query() or QueryRow() on this prepared statement. // ExpectQuery allows to expect Query() or QueryRow() on this prepared statement.
// this method is convenient in order to prevent duplicating sql query string matching. // this method is convenient in order to prevent duplicating sql query string matching.
func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery { func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery {
@ -300,7 +333,7 @@ type queryBasedExpectation struct {
args []driver.Value args []driver.Value
} }
func (e *queryBasedExpectation) attemptMatch(sql string, args []driver.Value) (err error) { func (e *queryBasedExpectation) attemptMatch(sql string, args []namedValue) (err error) {
if !e.queryMatches(sql) { if !e.queryMatches(sql) {
return fmt.Errorf(`could not match sql: "%s" with expected regexp "%s"`, sql, e.sqlRegex.String()) return fmt.Errorf(`could not match sql: "%s" with expected regexp "%s"`, sql, e.sqlRegex.String())
} }
@ -323,7 +356,7 @@ func (e *queryBasedExpectation) queryMatches(sql string) bool {
return e.sqlRegex.MatchString(sql) return e.sqlRegex.MatchString(sql)
} }
func (e *queryBasedExpectation) argsMatches(args []driver.Value) error { func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
if nil == e.args { if nil == e.args {
return nil return nil
} }
@ -334,14 +367,26 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
// custom argument matcher // custom argument matcher
matcher, ok := e.args[k].(Argument) matcher, ok := e.args[k].(Argument)
if ok { if ok {
if !matcher.Match(v) { // @TODO: does it make sense to pass value instead of named value?
if !matcher.Match(v.Value) {
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
} }
continue continue
} }
dval := e.args[k]
if named, isNamed := dval.(namedValue); isNamed {
dval = named.Value
if v.Name != named.Name {
return fmt.Errorf("named argument %d: name: \"%s\" does not match expected: \"%s\"", k, v.Name, named.Name)
}
if v.Ordinal != named.Ordinal {
return fmt.Errorf("named argument %d: ordinal position: \"%d\" does not match expected: \"%d\"", k, v.Ordinal, named.Ordinal)
}
}
// convert to driver converter // convert to driver converter
darg, err := driver.DefaultParameterConverter.ConvertValue(e.args[k]) darg, err := driver.DefaultParameterConverter.ConvertValue(dval)
if err != nil { if err != nil {
return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err) return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err)
} }
@ -350,8 +395,8 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg) return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
} }
if !reflect.DeepEqual(darg, args[k]) { if !reflect.DeepEqual(darg, v.Value) {
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, args[k], args[k]) return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value)
} }
} }
return nil return nil

View File

@ -10,29 +10,38 @@ import (
func TestQueryExpectationArgComparison(t *testing.T) { func TestQueryExpectationArgComparison(t *testing.T) {
e := &queryBasedExpectation{} e := &queryBasedExpectation{}
against := []driver.Value{int64(5)} against := []namedValue{{Value: int64(5), Ordinal: 1}}
if err := e.argsMatches(against); err != nil { if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err) t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err)
} }
e.args = []driver.Value{5, "str"} e.args = []driver.Value{5, "str"}
against = []driver.Value{int64(5)} against = []namedValue{{Value: int64(5), Ordinal: 1}}
if err := e.argsMatches(against); err == nil { if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the size is not the same") t.Error("arguments should not match, since the size is not the same")
} }
against = []driver.Value{int64(3), "str"} against = []namedValue{
{Value: int64(3), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err == nil { if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the first argument (int value) is different") t.Error("arguments should not match, since the first argument (int value) is different")
} }
against = []driver.Value{int64(5), "st"} against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "st", Ordinal: 2},
}
if err := e.argsMatches(against); err == nil { if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the second argument (string value) is different") t.Error("arguments should not match, since the second argument (string value) is different")
} }
against = []driver.Value{int64(5), "str"} against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err != nil { if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, but it did not: %s", err) t.Errorf("arguments should match, but it did not: %s", err)
} }
@ -41,7 +50,10 @@ func TestQueryExpectationArgComparison(t *testing.T) {
tm, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)") tm, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
e.args = []driver.Value{5, tm} e.args = []driver.Value{5, tm}
against = []driver.Value{int64(5), tm} against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: tm, Ordinal: 2},
}
if err := e.argsMatches(against); err != nil { if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, but it did not") t.Error("arguments should match, but it did not")
} }
@ -52,29 +64,95 @@ func TestQueryExpectationArgComparison(t *testing.T) {
} }
} }
func TestQueryExpectationNamedArgComparison(t *testing.T) {
e := &queryBasedExpectation{}
against := []namedValue{{Value: int64(5), Name: "id"}}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err)
}
e.args = []driver.Value{
namedValue{Name: "id", Value: int64(5)},
namedValue{Name: "s", Value: "str"},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the size is not the same")
}
against = []namedValue{
{Value: int64(5), Name: "id"},
{Value: "str", Name: "s"},
}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should have matched, but it did not: %v", err)
}
against = []namedValue{
{Value: int64(5), Name: "id"},
{Value: "str", Name: "username"},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments matched, but it should have not due to Name")
}
e.args = []driver.Value{
namedValue{Ordinal: 1, Value: int64(5)},
namedValue{Ordinal: 2, Value: "str"},
}
against = []namedValue{
{Value: int64(5), Ordinal: 0},
{Value: "str", Ordinal: 1},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments matched, but it should have not due to wrong Ordinal position")
}
against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should have matched, but it did not: %v", err)
}
}
func TestQueryExpectationArgComparisonBool(t *testing.T) { func TestQueryExpectationArgComparisonBool(t *testing.T) {
var e *queryBasedExpectation var e *queryBasedExpectation
e = &queryBasedExpectation{args: []driver.Value{true}} e = &queryBasedExpectation{args: []driver.Value{true}}
against := []driver.Value{true} against := []namedValue{
{Value: true, Ordinal: 1},
}
if err := e.argsMatches(against); err != nil { if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, since arguments are the same") t.Error("arguments should match, since arguments are the same")
} }
e = &queryBasedExpectation{args: []driver.Value{false}} e = &queryBasedExpectation{args: []driver.Value{false}}
against = []driver.Value{false} against = []namedValue{
{Value: false, Ordinal: 1},
}
if err := e.argsMatches(against); err != nil { if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, since argument are the same") t.Error("arguments should match, since argument are the same")
} }
e = &queryBasedExpectation{args: []driver.Value{true}} e = &queryBasedExpectation{args: []driver.Value{true}}
against = []driver.Value{false} against = []namedValue{
{Value: false, Ordinal: 1},
}
if err := e.argsMatches(against); err == nil { if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since argument is different") t.Error("arguments should not match, since argument is different")
} }
e = &queryBasedExpectation{args: []driver.Value{false}} e = &queryBasedExpectation{args: []driver.Value{false}}
against = []driver.Value{true} against = []namedValue{
{Value: true, Ordinal: 1},
}
if err := e.argsMatches(against); err == nil { if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since argument is different") t.Error("arguments should not match, since argument is different")
} }
@ -117,7 +195,7 @@ func TestBuildQuery(t *testing.T) {
name = 'John' name = 'John'
and and
address = 'Jakarta' address = 'Jakarta'
` `
mock.ExpectQuery(query) mock.ExpectQuery(query)

View File

@ -15,6 +15,7 @@ import (
"database/sql/driver" "database/sql/driver"
"fmt" "fmt"
"regexp" "regexp"
"time"
) )
// Sqlmock interface serves to create expectations // Sqlmock interface serves to create expectations
@ -184,6 +185,7 @@ func (c *sqlmock) Begin() (driver.Tx, error) {
expected.triggered = true expected.triggered = true
expected.Unlock() expected.Unlock()
defer time.Sleep(expected.delay)
return c, expected.err return c, expected.err
} }
@ -194,7 +196,18 @@ func (c *sqlmock) ExpectBegin() *ExpectedBegin {
} }
// Exec meets http://golang.org/pkg/database/sql/driver/#Execer // Exec meets http://golang.org/pkg/database/sql/driver/#Execer
func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, err error) { func (c *sqlmock) Exec(query string, args []driver.Value) (driver.Result, error) {
namedArgs := make([]namedValue, len(args))
for i, v := range args {
namedArgs[i] = namedValue{
Ordinal: i + 1,
Value: v,
}
}
return c.exec(nil, query, namedArgs)
}
func (c *sqlmock) exec(ctx interface{}, query string, args []namedValue) (res driver.Result, err error) {
query = stripQuery(query) query = stripQuery(query)
var expected *ExpectedExec var expected *ExpectedExec
var fulfilled int var fulfilled int
@ -230,17 +243,19 @@ func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er
return nil, fmt.Errorf(msg, query, args) return nil, fmt.Errorf(msg, query, args)
} }
defer expected.Unlock()
if !expected.queryMatches(query) { if !expected.queryMatches(query) {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', does not match regex '%s'", query, expected.sqlRegex.String()) return nil, fmt.Errorf("exec query '%s', does not match regex '%s'", query, expected.sqlRegex.String())
} }
if err := expected.argsMatches(args); err != nil { if err := expected.argsMatches(args); err != nil {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err) return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
} }
expected.triggered = true expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
if expected.err != nil { if expected.err != nil {
return nil, expected.err // mocked to return error return nil, expected.err // mocked to return error
@ -292,12 +307,14 @@ func (c *sqlmock) Prepare(query string) (driver.Stmt, error) {
} }
return nil, fmt.Errorf(msg, query) return nil, fmt.Errorf(msg, query)
} }
defer expected.Unlock()
if !expected.sqlRegex.MatchString(query) { if !expected.sqlRegex.MatchString(query) {
expected.Unlock()
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String()) return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
} }
expected.triggered = true expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
return &statement{c, query, expected.closeErr}, expected.err return &statement{c, query, expected.closeErr}, expected.err
} }
@ -308,8 +325,27 @@ func (c *sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare {
return e return e
} }
type namedValue struct {
Name string
Ordinal int
Value driver.Value
}
// Query meets http://golang.org/pkg/database/sql/driver/#Queryer // Query meets http://golang.org/pkg/database/sql/driver/#Queryer
func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err error) { func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err error) {
namedArgs := make([]namedValue, len(args))
for i, v := range args {
namedArgs[i] = namedValue{
Ordinal: i + 1,
Value: v,
}
}
return c.query(nil, query, namedArgs)
}
// in order to prevent dependencies, we use Context as a plain interface
// since it is only related to internal implementation
func (c *sqlmock) query(ctx interface{}, query string, args []namedValue) (rw driver.Rows, err error) {
query = stripQuery(query) query = stripQuery(query)
var expected *ExpectedQuery var expected *ExpectedQuery
var fulfilled int var fulfilled int
@ -346,18 +382,21 @@ func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err
return nil, fmt.Errorf(msg, query, args) return nil, fmt.Errorf(msg, query, args)
} }
defer expected.Unlock()
if !expected.queryMatches(query) { if !expected.queryMatches(query) {
expected.Unlock()
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String()) return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
} }
if err := expected.argsMatches(args); err != nil { if err := expected.argsMatches(args); err != nil {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err) return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
} }
expected.triggered = true expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
if expected.err != nil { if expected.err != nil {
return nil, expected.err // mocked to return error return nil, expected.err // mocked to return error
} }

5
sqlmock_go18.go Normal file
View File

@ -0,0 +1,5 @@
// +build go1.8
package sqlmock
// @TODO context based extensions