diff --git a/sqlmock_before_go18.go b/sqlmock_before_go18.go index 9965e78..0694976 100644 --- a/sqlmock_before_go18.go +++ b/sqlmock_before_go18.go @@ -51,6 +51,7 @@ func (c *sqlmock) query(query string, args []namedValue) (*ExpectedQuery, error) var expected *ExpectedQuery var fulfilled int var ok bool + var argMismatch error for _, next := range c.expected { next.Lock() if next.fulfilled() { @@ -74,6 +75,10 @@ func (c *sqlmock) query(query string, args []namedValue) (*ExpectedQuery, error) if err := qr.attemptArgMatch(args); err == nil { expected = qr break + } else { + // remember the reason the sql matched but the args did not, + // so it can be reported if no other expectation matches + argMismatch = err } } next.Unlock() @@ -84,7 +89,11 @@ func (c *sqlmock) query(query string, args []namedValue) (*ExpectedQuery, error) if fulfilled == len(c.expected) { msg = "all expectations were already fulfilled, " + msg } - return nil, fmt.Errorf(msg, query, args) + err := fmt.Errorf(msg, query, args) + if argMismatch != nil { + err = fmt.Errorf("%s, arguments do not match: %s", err, argMismatch) + } + return nil, err } defer expected.Unlock() @@ -133,6 +142,7 @@ func (c *sqlmock) exec(query string, args []namedValue) (*ExpectedExec, error) { var expected *ExpectedExec var fulfilled int var ok bool + var argMismatch error for _, next := range c.expected { next.Lock() if next.fulfilled() { @@ -157,6 +167,10 @@ func (c *sqlmock) exec(query string, args []namedValue) (*ExpectedExec, error) { if err := exec.attemptArgMatch(args); err == nil { expected = exec break + } else { + // remember the reason the sql matched but the args did not, + // so it can be reported if no other expectation matches + argMismatch = err } } next.Unlock() @@ -166,7 +180,11 @@ func (c *sqlmock) exec(query string, args []namedValue) (*ExpectedExec, error) { if fulfilled == len(c.expected) { msg = "all expectations were already fulfilled, " + msg } - return nil, fmt.Errorf(msg, query, args) + err := fmt.Errorf(msg, query, args) + if argMismatch != nil { + err = fmt.Errorf("%s, arguments do not match: %s", err, argMismatch) + } + return nil, err } defer expected.Unlock() diff --git a/sqlmock_go18.go b/sqlmock_go18.go index 9644958..4050322 100644 --- a/sqlmock_go18.go +++ b/sqlmock_go18.go @@ -200,6 +200,7 @@ func (c *sqlmock) query(query string, args []driver.NamedValue) (*ExpectedQuery, var expected *ExpectedQuery var fulfilled int var ok bool + var argMismatch error for _, next := range c.expected { next.Lock() if next.fulfilled() { @@ -223,6 +224,10 @@ func (c *sqlmock) query(query string, args []driver.NamedValue) (*ExpectedQuery, if err := qr.attemptArgMatch(args); err == nil { expected = qr break + } else { + // remember the reason the sql matched but the args did not, + // so it can be reported if no other expectation matches + argMismatch = err } } next.Unlock() @@ -233,7 +238,11 @@ func (c *sqlmock) query(query string, args []driver.NamedValue) (*ExpectedQuery, if fulfilled == len(c.expected) { msg = "all expectations were already fulfilled, " + msg } - return nil, fmt.Errorf(msg, query, args) + err := fmt.Errorf(msg, query, args) + if argMismatch != nil { + err = fmt.Errorf("%s, arguments do not match: %s", err, argMismatch) + } + return nil, err } defer expected.Unlock() @@ -283,6 +292,7 @@ func (c *sqlmock) exec(query string, args []driver.NamedValue) (*ExpectedExec, e var expected *ExpectedExec var fulfilled int var ok bool + var argMismatch error for _, next := range c.expected { next.Lock() if next.fulfilled() { @@ -307,6 +317,10 @@ func (c *sqlmock) exec(query string, args []driver.NamedValue) (*ExpectedExec, e if err := exec.attemptArgMatch(args); err == nil { expected = exec break + } else { + // remember the reason the sql matched but the args did not, + // so it can be reported if no other expectation matches + argMismatch = err } } next.Unlock() @@ -316,7 +330,11 @@ func (c *sqlmock) exec(query string, args []driver.NamedValue) (*ExpectedExec, e if fulfilled == len(c.expected) { msg = "all expectations were already fulfilled, " + msg } - return nil, fmt.Errorf(msg, query, args) + err := fmt.Errorf(msg, query, args) + if argMismatch != nil { + err = fmt.Errorf("%s, arguments do not match: %s", err, argMismatch) + } + return nil, err } defer expected.Unlock() diff --git a/sqlmock_go18_test.go b/sqlmock_go18_test.go index ddc7306..4c2ee8f 100644 --- a/sqlmock_go18_test.go +++ b/sqlmock_go18_test.go @@ -7,10 +7,49 @@ import ( "context" "database/sql" "errors" + "strings" "testing" "time" ) +// When expectations are matched out of order and a query matches by sql but +// not by arguments, the failure should explain the argument mismatch and +// include the actual received values, not just report "was not expected". +func TestUnorderedArgumentMismatchReportsReceivedValues(t *testing.T) { + t.Parallel() + db, mock, err := New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + mock.MatchExpectationsInOrder(false) + + mock.ExpectExec("UPDATE users").WithArgs("alice").WillReturnResult(NewResult(1, 1)) + if _, err := db.Exec("UPDATE users SET name = ?", "bob"); err == nil { + t.Error("expected an error for the mismatched exec argument, but got none") + } else { + if !strings.Contains(err.Error(), "arguments do not match") { + t.Errorf("expected the error to explain the argument mismatch, but got: %s", err) + } + if !strings.Contains(err.Error(), "bob") { + t.Errorf("expected the error to include the received argument value 'bob', but got: %s", err) + } + } + + mock.ExpectQuery("SELECT name FROM users").WithArgs(1).WillReturnRows(NewRows([]string{"name"}).AddRow("alice")) + if _, err := db.Query("SELECT name FROM users WHERE id = ?", 2); err == nil { + t.Error("expected an error for the mismatched query argument, but got none") + } else { + if !strings.Contains(err.Error(), "arguments do not match") { + t.Errorf("expected the error to explain the argument mismatch, but got: %s", err) + } + if !strings.Contains(err.Error(), "actual [int64 - 2]") { + t.Errorf("expected the error to include the received argument value 2, but got: %s", err) + } + } +} + func TestContextExecCancel(t *testing.T) { t.Parallel() db, mock, err := New()