From d11f623794a97c1d6b50814ac3d60a0959b85f68 Mon Sep 17 00:00:00 2001 From: gedi Date: Mon, 6 Feb 2017 14:38:57 +0200 Subject: [PATCH] implements named arguments support and adds delay expectation for context deadline simulation --- expectations.go | 59 ++++++++++++++++++++++--- expectations_test.go | 100 ++++++++++++++++++++++++++++++++++++++----- sqlmock.go | 51 +++++++++++++++++++--- sqlmock_go18.go | 5 +++ 4 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 sqlmock_go18.go diff --git a/expectations.go b/expectations.go index 5b6865e..3902ff3 100644 --- a/expectations.go +++ b/expectations.go @@ -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 diff --git a/expectations_test.go b/expectations_test.go index 032f029..6238532 100644 --- a/expectations_test.go +++ b/expectations_test.go @@ -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) diff --git a/sqlmock.go b/sqlmock.go index 7ac8076..500b5c6 100644 --- a/sqlmock.go +++ b/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 } diff --git a/sqlmock_go18.go b/sqlmock_go18.go new file mode 100644 index 0000000..2021007 --- /dev/null +++ b/sqlmock_go18.go @@ -0,0 +1,5 @@ +// +build go1.8 + +package sqlmock + +// @TODO context based extensions