diff --git a/.travis.yml b/.travis.yml index 3c1f13e..75882bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ go: - 1.3 - 1.4 - 1.5 - - release + - 1.6 - tip script: go test -race diff --git a/README.md b/README.md index cbec043..a386b44 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,11 @@ It only asserts that argument is of `time.Time` type. ## Changes +- **2016-02-23** - added **sqlmock.AnyArg()** function to provide any kind + of argument matcher. +- **2016-02-23** - convert expected arguments to driver.Value as natural + driver does, the change may affect time.Time comparison and will be + stricter. See [issue](https://github.com/DATA-DOG/go-sqlmock/issues/31). - **2015-08-27** - **v1** api change, concurrency support, all known issues fixed. - **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error - **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for diff --git a/argument.go b/argument.go new file mode 100644 index 0000000..7727481 --- /dev/null +++ b/argument.go @@ -0,0 +1,24 @@ +package sqlmock + +import "database/sql/driver" + +// Argument interface allows to match +// any argument in specific way when used with +// ExpectedQuery and ExpectedExec expectations. +type Argument interface { + Match(driver.Value) bool +} + +// AnyArg will return an Argument which can +// match any kind of arguments. +// +// Useful for time.Time or similar kinds of arguments. +func AnyArg() Argument { + return anyArgument{} +} + +type anyArgument struct{} + +func (a anyArgument) Match(_ driver.Value) bool { + return true +} diff --git a/expectations.go b/expectations.go index 63e6dc3..62b8559 100644 --- a/expectations.go +++ b/expectations.go @@ -3,19 +3,11 @@ package sqlmock import ( "database/sql/driver" "fmt" - "reflect" "regexp" "strings" "sync" ) -// Argument interface allows to match -// any argument in specific way when used with -// ExpectedQuery and ExpectedExec expectations. -type Argument interface { - Match(driver.Value) bool -} - // an expectation interface type expectation interface { fulfilled() bool @@ -307,16 +299,22 @@ type queryBasedExpectation struct { args []driver.Value } -func (e *queryBasedExpectation) attemptMatch(sql string, args []driver.Value) (ret bool) { +func (e *queryBasedExpectation) attemptMatch(sql string, args []driver.Value) (err error) { if !e.queryMatches(sql) { - return + return fmt.Errorf(`could not match sql: "%s" with expected regexp "%s"`, sql, e.sqlRegex.String()) } - defer recover() // ignore panic since we attempt a match + // catch panic + defer func() { + if e := recover(); e != nil { + _, ok := e.(error) + if !ok { + err = fmt.Errorf(e.(string)) + } + } + }() - if e.argsMatches(args) { - return true - } + err = e.argsMatches(args) return } @@ -324,50 +322,36 @@ func (e *queryBasedExpectation) queryMatches(sql string) bool { return e.sqlRegex.MatchString(sql) } -func (e *queryBasedExpectation) argsMatches(args []driver.Value) bool { +func (e *queryBasedExpectation) argsMatches(args []driver.Value) error { if nil == e.args { - return true + return nil } if len(args) != len(e.args) { - return false + return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args)) } for k, v := range args { + // custom argument matcher matcher, ok := e.args[k].(Argument) if ok { if !matcher.Match(v) { - return false + return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k]) } continue } - vi := reflect.ValueOf(v) - ai := reflect.ValueOf(e.args[k]) - switch vi.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if vi.Int() != ai.Int() { - return false - } - case reflect.Float32, reflect.Float64: - if vi.Float() != ai.Float() { - return false - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if vi.Uint() != ai.Uint() { - return false - } - case reflect.String: - if vi.String() != ai.String() { - return false - } - case reflect.Bool: - if vi.Bool() != ai.Bool() { - return false - } - default: - // compare types like time.Time based on type only - if vi.Kind() != ai.Kind() { - return false - } + + // convert to driver converter + darg, err := driver.DefaultParameterConverter.ConvertValue(e.args[k]) + 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) + } + + if !driver.IsValue(darg) { + return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg) + } + + if darg != args[k] { + return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, args[k], args[k]) } } - return true + return nil } diff --git a/expectations_test.go b/expectations_test.go index d973c68..6fd3435 100644 --- a/expectations_test.go +++ b/expectations_test.go @@ -8,55 +8,47 @@ import ( "time" ) -type matcher struct { -} - -func (m matcher) Match(driver.Value) bool { - return true -} - func TestQueryExpectationArgComparison(t *testing.T) { e := &queryBasedExpectation{} - against := []driver.Value{5} - if !e.argsMatches(against) { - t.Error("arguments should match, since the no expectation was set") + against := []driver.Value{int64(5)} + 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{5} - if e.argsMatches(against) { + against = []driver.Value{int64(5)} + if err := e.argsMatches(against); err == nil { t.Error("arguments should not match, since the size is not the same") } - against = []driver.Value{3, "str"} - if e.argsMatches(against) { + against = []driver.Value{int64(3), "str"} + if err := e.argsMatches(against); err == nil { t.Error("arguments should not match, since the first argument (int value) is different") } - against = []driver.Value{5, "st"} - if e.argsMatches(against) { + against = []driver.Value{int64(5), "st"} + if err := e.argsMatches(against); err == nil { t.Error("arguments should not match, since the second argument (string value) is different") } - against = []driver.Value{5, "str"} - if !e.argsMatches(against) { - t.Error("arguments should match, but it did not") + against = []driver.Value{int64(5), "str"} + if err := e.argsMatches(against); err != nil { + t.Errorf("arguments should match, but it did not: %s", err) } - e.args = []driver.Value{5, time.Now()} - const longForm = "Jan 2, 2006 at 3:04pm (MST)" tm, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)") + e.args = []driver.Value{5, tm} - against = []driver.Value{5, tm} - if !e.argsMatches(against) { - t.Error("arguments should match (time will be compared only by type), but it did not") + against = []driver.Value{int64(5), tm} + if err := e.argsMatches(against); err != nil { + t.Error("arguments should match, but it did not") } - against = []driver.Value{5, matcher{}} - if !e.argsMatches(against) { - t.Error("arguments should match, but it did not") + e.args = []driver.Value{5, AnyArg()} + if err := e.argsMatches(against); err != nil { + t.Errorf("arguments should match, but it did not: %s", err) } } @@ -65,25 +57,25 @@ func TestQueryExpectationArgComparisonBool(t *testing.T) { e = &queryBasedExpectation{args: []driver.Value{true}} against := []driver.Value{true} - if !e.argsMatches(against) { + 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} - if !e.argsMatches(against) { + 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} - if e.argsMatches(against) { + 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} - if e.argsMatches(against) { + if err := e.argsMatches(against); err == nil { t.Error("arguments should not match, since argument is different") } } diff --git a/sqlmock.go b/sqlmock.go index a11da7c..e82944b 100644 --- a/sqlmock.go +++ b/sqlmock.go @@ -14,7 +14,6 @@ import ( "database/sql" "database/sql/driver" "fmt" - "reflect" "regexp" ) @@ -216,7 +215,7 @@ func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er 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) { + if err := exec.attemptMatch(query, args); err == nil { expected = exec break } @@ -233,24 +232,13 @@ func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er 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 err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err) } if expected.err != nil { @@ -335,7 +323,7 @@ func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err 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) { + if err := qr.attemptMatch(query, args); err == nil { expected = qr break } @@ -353,24 +341,13 @@ func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err 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 err := expected.argsMatches(args); err != nil { + return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err) } if expected.err != nil { diff --git a/statement_test.go b/statement_test.go new file mode 100644 index 0000000..1cb3583 --- /dev/null +++ b/statement_test.go @@ -0,0 +1,33 @@ +// +build go1.6 + +package sqlmock + +import ( + "errors" + "testing" +) + +func TestExpectedPreparedStatemtCloseError(t *testing.T) { + conn, mock, err := New() + if err != nil { + t.Fatalf("failed to open sqlmock database:", err) + } + + mock.ExpectBegin() + want := errors.New("STMT ERROR") + mock.ExpectPrepare("SELECT").WillReturnCloseError(want) + + txn, err := conn.Begin() + if err != nil { + t.Fatalf("unexpected error while opening transaction:", err) + } + + stmt, err := txn.Prepare("SELECT") + if err != nil { + t.Fatalf("unexpected error while preparing a statement:", err) + } + + if err := stmt.Close(); err != want { + t.Fatalf("Got = %v, want = %v", err, want) + } +}