diff --git a/.travis.yml b/.travis.yml index 76f71f0..8d7ddf0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: go +sudo: false go: - 1.2 - 1.3 @@ -9,4 +10,3 @@ go: script: - go test -v ./... - go test -race ./... - # linter will follow diff --git a/expectations.go b/expectations.go index e4f2a2c..55cef3c 100644 --- a/expectations.go +++ b/expectations.go @@ -2,8 +2,10 @@ package sqlmock import ( "database/sql/driver" + "fmt" "reflect" "regexp" + "strings" "sync" ) @@ -19,6 +21,7 @@ type expectation interface { fulfilled() bool Lock() Unlock() + String() string } // common expectation struct @@ -45,6 +48,15 @@ func (e *ExpectedClose) WillReturnError(err error) *ExpectedClose { return e } +// String returns string representation +func (e *ExpectedClose) String() string { + msg := "ExpectedClose => expecting database Close" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + // ExpectedBegin is used to manage *sql.DB.Begin expectation // returned by *Sqlmock.ExpectBegin. type ExpectedBegin struct { @@ -57,6 +69,15 @@ func (e *ExpectedBegin) WillReturnError(err error) *ExpectedBegin { return e } +// String returns string representation +func (e *ExpectedBegin) String() string { + msg := "ExpectedBegin => expecting database transaction Begin" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + // ExpectedCommit is used to manage *sql.Tx.Commit expectation // returned by *Sqlmock.ExpectCommit. type ExpectedCommit struct { @@ -69,6 +90,15 @@ func (e *ExpectedCommit) WillReturnError(err error) *ExpectedCommit { return e } +// String returns string representation +func (e *ExpectedCommit) String() string { + msg := "ExpectedCommit => expecting transaction Commit" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + // ExpectedRollback is used to manage *sql.Tx.Rollback expectation // returned by *Sqlmock.ExpectRollback. type ExpectedRollback struct { @@ -81,6 +111,15 @@ func (e *ExpectedRollback) WillReturnError(err error) *ExpectedRollback { return e } +// String returns string representation +func (e *ExpectedRollback) String() string { + msg := "ExpectedRollback => expecting transaction Rollback" + if e.err != nil { + msg += fmt.Sprintf(", which should return error: %s", e.err) + } + return msg +} + // ExpectedQuery is used to manage *sql.DB.Query, *dql.DB.QueryRow, *sql.Tx.Query, // *sql.Tx.QueryRow, *sql.Stmt.Query or *sql.Stmt.QueryRow expectations. // Returned by *Sqlmock.ExpectQuery. @@ -110,6 +149,37 @@ func (e *ExpectedQuery) WillReturnRows(rows driver.Rows) *ExpectedQuery { return e } +// String returns string representation +func (e *ExpectedQuery) String() string { + msg := "ExpectedQuery => expecting Query or QueryRow which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + for i, arg := range e.args { + msg += fmt.Sprintf(" %d - %+v\n", i, arg) + } + msg = strings.TrimSpace(msg) + } + + if e.rows != nil { + msg += "\n - should return rows:\n" + rs, _ := e.rows.(*rows) + for i, row := range rs.rows { + msg += fmt.Sprintf(" %d - %+v\n", i, row) + } + msg = strings.TrimSpace(msg) + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + // ExpectedExec is used to manage *sql.DB.Exec, *sql.Tx.Exec or *sql.Stmt.Exec expectations. // Returned by *Sqlmock.ExpectExec. type ExpectedExec struct { @@ -131,6 +201,39 @@ func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec { return e } +// String returns string representation +func (e *ExpectedExec) String() string { + msg := "ExpectedExec => expecting Exec which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if len(e.args) == 0 { + msg += "\n - is without arguments" + } else { + msg += "\n - is with arguments:\n" + var margs []string + for i, arg := range e.args { + margs = append(margs, fmt.Sprintf(" %d - %+v", i, arg)) + } + msg += strings.Join(margs, "\n") + } + + if e.result != nil { + res, _ := e.result.(*result) + msg += "\n - should return Result having:" + msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID) + msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected) + if res.err != nil { + msg += fmt.Sprintf("\n Error: %s", res.err) + } + } + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + return msg +} + // WillReturnResult arranges for an expected Exec() to return a particular // result, there is sqlmock.NewResult(lastInsertID int64, affectedRows int64) method // to build a corresponding result. Or if actions needs to be tested against errors @@ -180,6 +283,22 @@ func (e *ExpectedPrepare) ExpectExec() *ExpectedExec { return eq } +// String returns string representation +func (e *ExpectedPrepare) String() string { + msg := "ExpectedPrepare => expecting Prepare statement which:" + msg += "\n - matches sql: '" + e.sqlRegex.String() + "'" + + if e.err != nil { + msg += fmt.Sprintf("\n - should return error: %s", e.err) + } + + if e.closeErr != nil { + msg += fmt.Sprintf("\n - should return error on Close: %s", e.closeErr) + } + + return msg +} + // query based expectation // adds a query matching logic type queryBasedExpectation struct { diff --git a/result_test.go b/result_test.go index b735c87..e02e5e3 100644 --- a/result_test.go +++ b/result_test.go @@ -23,7 +23,12 @@ func ExampleNewResult() { result := NewResult(lastInsertID, affected) mock.ExpectExec("^INSERT (.+)").WillReturnResult(result) fmt.Println(mock.ExpectationsWereMet()) - // Output: there is a remaining expectation *sqlmock.ExpectedExec which was not matched yet + // Output: there is a remaining expectation which was not matched: ExpectedExec => expecting Exec which: + // - matches sql: '^INSERT (.+)' + // - is without arguments + // - should return Result having: + // LastInsertId: 0 + // RowsAffected: 0 } func TestShouldReturnValidSqlDriverResult(t *testing.T) { diff --git a/sqlmock.go b/sqlmock.go index 8490590..605575b 100644 --- a/sqlmock.go +++ b/sqlmock.go @@ -1,12 +1,12 @@ /* -Package sqlmock provides sql driver mock connecection, which allows to test database, -create expectations and ensure the correct execution flow of any database operations. -It hooks into Go standard library's database/sql package. +Package sqlmock provides sql driver connection, which allows to test database +interactions by expected calls and simulate their results or errors. -The package provides convenient methods to mock database queries, transactions and -expect the right execution flow, compare query arguments or even return error instead -to simulate failures. See the example bellow, which illustrates how convenient it is -to work with. +It does not require any modifications to your source code in order to test +and mock database operations. + +The driver allows to mock any sql driver method behavior. Concurrent actions +are also supported. */ package sqlmock @@ -61,11 +61,13 @@ func (c *Sqlmock) Close() error { } var expected *ExpectedClose + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -75,11 +77,16 @@ func (c *Sqlmock) Close() error { next.Unlock() if c.MatchExpectationsInOrder { - return fmt.Errorf("call to database Close, was not expected, next expectation is %T as %+v", next, next) + return fmt.Errorf("call to database Close, was not expected, next expectation is: %s", next) } } + if expected == nil { - return fmt.Errorf("all expectations were already fulfilled, call to database Close was not expected") + msg := "call to database Close was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) } expected.triggered = true @@ -92,7 +99,7 @@ func (c *Sqlmock) Close() error { func (c *Sqlmock) ExpectationsWereMet() error { for _, e := range c.expected { if !e.fulfilled() { - return fmt.Errorf("there is a remaining expectation %T which was not matched yet", e) + return fmt.Errorf("there is a remaining expectation which was not matched: %s", e) } } return nil @@ -102,10 +109,12 @@ func (c *Sqlmock) ExpectationsWereMet() error { func (c *Sqlmock) Begin() (driver.Tx, error) { var expected *ExpectedBegin var ok bool + var fulfilled int for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -115,11 +124,15 @@ func (c *Sqlmock) Begin() (driver.Tx, error) { next.Unlock() if c.MatchExpectationsInOrder { - return nil, fmt.Errorf("call to begin transaction, was not expected, next expectation is %T as %+v", next, next) + return nil, fmt.Errorf("call to database transaction Begin, was not expected, next expectation is: %s", next) } } if expected == nil { - return nil, fmt.Errorf("all expectations were already fulfilled, call to begin transaction was not expected") + msg := "call to database transaction Begin was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg) } expected.triggered = true @@ -139,11 +152,13 @@ func (c *Sqlmock) ExpectBegin() *ExpectedBegin { func (c *Sqlmock) Exec(query string, args []driver.Value) (res driver.Result, err error) { query = stripQuery(query) var expected *ExpectedExec + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -152,7 +167,7 @@ func (c *Sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er break } next.Unlock() - return nil, fmt.Errorf("call to exec query '%s' with args %+v, was not expected, next expectation is %T as %+v", query, args, next, next) + 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) { @@ -163,7 +178,11 @@ func (c *Sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er next.Unlock() } if expected == nil { - return nil, fmt.Errorf("all expectations were already fulfilled, call to exec '%s' query with args %+v was not expected", query, args) + msg := "call to exec '%s' query with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) } defer expected.Unlock() @@ -172,8 +191,8 @@ func (c *Sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er 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 expected arguments \"%+v\", reason %s" - *errp = fmt.Errorf(msg, q, a, exp.args, se) + 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 } @@ -211,11 +230,13 @@ func (c *Sqlmock) ExpectExec(sqlRegexStr string) *ExpectedExec { // Prepare meets http://golang.org/pkg/database/sql/driver/#Conn interface func (c *Sqlmock) Prepare(query string) (driver.Stmt, error) { var expected *ExpectedPrepare + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -225,13 +246,17 @@ func (c *Sqlmock) Prepare(query string) (driver.Stmt, error) { next.Unlock() if c.MatchExpectationsInOrder { - return nil, fmt.Errorf("call to Prepare stetement with query '%s', was not expected, next expectation is %T as %+v", query, next, next) + return nil, fmt.Errorf("call to Prepare stetement with query '%s', was not expected, next expectation is: %s", query, next) } } query = stripQuery(query) if expected == nil { - return nil, fmt.Errorf("all expectations were already fulfilled, call to Prepare '%s' query was not expected", query) + msg := "call to Prepare '%s' query was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query) } expected.triggered = true @@ -254,11 +279,13 @@ func (c *Sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare { func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err error) { query = stripQuery(query) var expected *ExpectedQuery + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -267,7 +294,7 @@ func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err break } next.Unlock() - return nil, fmt.Errorf("call to query '%s' with args %+v, was not expected, next expectation is %T as %+v", query, args, next, next) + 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) { @@ -277,8 +304,13 @@ func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err } next.Unlock() } + if expected == nil { - return nil, fmt.Errorf("all expectations were already fulfilled, call to query '%s' with args %+v was not expected", query, args) + msg := "call to query '%s' with args %+v was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return nil, fmt.Errorf(msg, query, args) } defer expected.Unlock() @@ -287,8 +319,8 @@ func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err 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 expected arguments \"%+v\", reason %s" - *errp = fmt.Errorf(msg, q, a, exp.args, se) + 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 } @@ -343,11 +375,13 @@ func (c *Sqlmock) ExpectRollback() *ExpectedRollback { // Commit meets http://golang.org/pkg/database/sql/driver/#Tx func (c *Sqlmock) Commit() error { var expected *ExpectedCommit + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -357,11 +391,15 @@ func (c *Sqlmock) Commit() error { next.Unlock() if c.MatchExpectationsInOrder { - return fmt.Errorf("call to commit transaction, was not expected, next expectation is %T as %+v", next, next) + return fmt.Errorf("call to commit transaction, was not expected, next expectation is: %s", next) } } if expected == nil { - return fmt.Errorf("all expectations were already fulfilled, call to commit transaction was not expected") + msg := "call to commit transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) } expected.triggered = true @@ -372,11 +410,13 @@ func (c *Sqlmock) Commit() error { // Rollback meets http://golang.org/pkg/database/sql/driver/#Tx func (c *Sqlmock) Rollback() error { var expected *ExpectedRollback + var fulfilled int var ok bool for _, next := range c.expected { next.Lock() if next.fulfilled() { next.Unlock() + fulfilled++ continue } @@ -386,11 +426,15 @@ func (c *Sqlmock) Rollback() error { next.Unlock() if c.MatchExpectationsInOrder { - return fmt.Errorf("call to rollback transaction, was not expected, next expectation is %T as %+v", next, next) + return fmt.Errorf("call to rollback transaction, was not expected, next expectation is: %s", next) } } if expected == nil { - return fmt.Errorf("all expectations were already fulfilled, call to rollback transaction was not expected") + msg := "call to rollback transaction was not expected" + if fulfilled == len(c.expected) { + msg = "all expectations were already fulfilled, " + msg + } + return fmt.Errorf(msg) } expected.triggered = true