mirror of
https://github.com/zhashkevych/go-sqlxmock.git
synced 2024-11-24 08:12:13 +02:00
implements named arguments support and adds delay expectation for context deadline simulation
This commit is contained in:
parent
55ecc5a333
commit
d11f623794
@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// an expectation interface
|
||||
@ -54,6 +55,7 @@ func (e *ExpectedClose) String() string {
|
||||
// returned by *Sqlmock.ExpectBegin.
|
||||
type ExpectedBegin struct {
|
||||
commonExpectation
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for *sql.DB.Begin action
|
||||
@ -71,6 +73,13 @@ func (e *ExpectedBegin) String() string {
|
||||
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
|
||||
// returned by *Sqlmock.ExpectCommit.
|
||||
type ExpectedCommit struct {
|
||||
@ -118,7 +127,8 @@ func (e *ExpectedRollback) String() string {
|
||||
// Returned by *Sqlmock.ExpectQuery.
|
||||
type ExpectedQuery struct {
|
||||
queryBasedExpectation
|
||||
rows driver.Rows
|
||||
rows driver.Rows
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (e *ExpectedQuery) String() string {
|
||||
msg := "ExpectedQuery => expecting Query or QueryRow which:"
|
||||
@ -178,6 +195,7 @@ func (e *ExpectedQuery) String() string {
|
||||
type ExpectedExec struct {
|
||||
queryBasedExpectation
|
||||
result driver.Result
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (e *ExpectedExec) String() string {
|
||||
msg := "ExpectedExec => expecting Exec which:"
|
||||
@ -244,6 +269,7 @@ type ExpectedPrepare struct {
|
||||
sqlRegex *regexp.Regexp
|
||||
statement driver.Stmt
|
||||
closeErr error
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// this method is convenient in order to prevent duplicating sql query string matching.
|
||||
func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery {
|
||||
@ -300,7 +333,7 @@ type queryBasedExpectation struct {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
|
||||
func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
|
||||
if nil == e.args {
|
||||
return nil
|
||||
}
|
||||
@ -334,14 +367,26 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
|
||||
// custom argument matcher
|
||||
matcher, ok := e.args[k].(Argument)
|
||||
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])
|
||||
}
|
||||
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
|
||||
darg, err := driver.DefaultParameterConverter.ConvertValue(e.args[k])
|
||||
darg, err := driver.DefaultParameterConverter.ConvertValue(dval)
|
||||
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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(darg, args[k]) {
|
||||
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, args[k], 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, v.Value, v.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -10,29 +10,38 @@ import (
|
||||
|
||||
func TestQueryExpectationArgComparison(t *testing.T) {
|
||||
e := &queryBasedExpectation{}
|
||||
against := []driver.Value{int64(5)}
|
||||
against := []namedValue{{Value: int64(5), Ordinal: 1}}
|
||||
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{5, "str"}
|
||||
|
||||
against = []driver.Value{int64(5)}
|
||||
against = []namedValue{{Value: int64(5), Ordinal: 1}}
|
||||
if err := e.argsMatches(against); err == nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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)")
|
||||
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 {
|
||||
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) {
|
||||
var e *queryBasedExpectation
|
||||
|
||||
e = &queryBasedExpectation{args: []driver.Value{true}}
|
||||
against := []driver.Value{true}
|
||||
against := []namedValue{
|
||||
{Value: true, Ordinal: 1},
|
||||
}
|
||||
if err := e.argsMatches(against); err != nil {
|
||||
t.Error("arguments should match, since arguments are the same")
|
||||
}
|
||||
|
||||
e = &queryBasedExpectation{args: []driver.Value{false}}
|
||||
against = []driver.Value{false}
|
||||
against = []namedValue{
|
||||
{Value: false, Ordinal: 1},
|
||||
}
|
||||
if err := e.argsMatches(against); err != nil {
|
||||
t.Error("arguments should match, since argument are the same")
|
||||
}
|
||||
|
||||
e = &queryBasedExpectation{args: []driver.Value{true}}
|
||||
against = []driver.Value{false}
|
||||
against = []namedValue{
|
||||
{Value: false, Ordinal: 1},
|
||||
}
|
||||
if err := e.argsMatches(against); err == nil {
|
||||
t.Error("arguments should not match, since argument is different")
|
||||
}
|
||||
|
||||
e = &queryBasedExpectation{args: []driver.Value{false}}
|
||||
against = []driver.Value{true}
|
||||
against = []namedValue{
|
||||
{Value: true, Ordinal: 1},
|
||||
}
|
||||
if err := e.argsMatches(against); err == nil {
|
||||
t.Error("arguments should not match, since argument is different")
|
||||
}
|
||||
@ -117,7 +195,7 @@ func TestBuildQuery(t *testing.T) {
|
||||
name = 'John'
|
||||
and
|
||||
address = 'Jakarta'
|
||||
|
||||
|
||||
`
|
||||
|
||||
mock.ExpectQuery(query)
|
||||
|
51
sqlmock.go
51
sqlmock.go
@ -15,6 +15,7 @@ import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Sqlmock interface serves to create expectations
|
||||
@ -184,6 +185,7 @@ func (c *sqlmock) Begin() (driver.Tx, error) {
|
||||
|
||||
expected.triggered = true
|
||||
expected.Unlock()
|
||||
defer time.Sleep(expected.delay)
|
||||
return c, expected.err
|
||||
}
|
||||
|
||||
@ -194,7 +196,18 @@ func (c *sqlmock) ExpectBegin() *ExpectedBegin {
|
||||
}
|
||||
|
||||
// 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)
|
||||
var expected *ExpectedExec
|
||||
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)
|
||||
}
|
||||
|
||||
defer expected.Unlock()
|
||||
|
||||
if !expected.queryMatches(query) {
|
||||
expected.Unlock()
|
||||
return nil, fmt.Errorf("exec query '%s', does not match regex '%s'", query, expected.sqlRegex.String())
|
||||
}
|
||||
|
||||
if err := expected.argsMatches(args); err != nil {
|
||||
expected.Unlock()
|
||||
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
|
||||
}
|
||||
|
||||
expected.triggered = true
|
||||
defer time.Sleep(expected.delay)
|
||||
defer expected.Unlock()
|
||||
|
||||
if expected.err != nil {
|
||||
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)
|
||||
}
|
||||
defer expected.Unlock()
|
||||
if !expected.sqlRegex.MatchString(query) {
|
||||
expected.Unlock()
|
||||
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
|
||||
}
|
||||
|
||||
expected.triggered = true
|
||||
defer time.Sleep(expected.delay)
|
||||
defer expected.Unlock()
|
||||
return &statement{c, query, expected.closeErr}, expected.err
|
||||
}
|
||||
|
||||
@ -308,8 +325,27 @@ func (c *sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare {
|
||||
return e
|
||||
}
|
||||
|
||||
type namedValue struct {
|
||||
Name string
|
||||
Ordinal int
|
||||
Value driver.Value
|
||||
}
|
||||
|
||||
// Query meets http://golang.org/pkg/database/sql/driver/#Queryer
|
||||
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)
|
||||
var expected *ExpectedQuery
|
||||
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)
|
||||
}
|
||||
|
||||
defer expected.Unlock()
|
||||
|
||||
if !expected.queryMatches(query) {
|
||||
expected.Unlock()
|
||||
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
|
||||
}
|
||||
|
||||
if err := expected.argsMatches(args); err != nil {
|
||||
expected.Unlock()
|
||||
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
|
||||
}
|
||||
|
||||
expected.triggered = true
|
||||
|
||||
defer time.Sleep(expected.delay)
|
||||
defer expected.Unlock()
|
||||
|
||||
if expected.err != nil {
|
||||
return nil, expected.err // mocked to return error
|
||||
}
|
||||
|
5
sqlmock_go18.go
Normal file
5
sqlmock_go18.go
Normal file
@ -0,0 +1,5 @@
|
||||
// +build go1.8
|
||||
|
||||
package sqlmock
|
||||
|
||||
// @TODO context based extensions
|
Loading…
Reference in New Issue
Block a user