diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d0a2e8f --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) + +Copyright (c) 2013, DataDog.lt team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name DataDog.lt may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0eeaa95..5457479 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,300 @@ +# Sql driver mock for Golang -db = mock.Open("test", "") +This is a **mock** driver as **database/sql/driver** which is very flexible and pragmatic to +manage and mock expected queries. All the expectations should be met and all queries and actions +triggered should be mocked in order to pass a test. -db.ExpectTransactionBegin() -db.ExpectTransactionBegin().WillReturnError("some error") -db.ExpectQuery("SELECT bla").With(5, 8, "stat").WillReturnNone() -db.ExpectExec("UPDATE tbl SET").With(5, "val").WillReturnResult(res /* sql.Result */) -db.ExpectExec("INSERT INTO bla").With(5, 8, "stat").WillReturnResult(res /* sql.Result */) -db.ExpectQuery("SELECT bla").With(5, 8, "stat").WillReturnRows() +## Install + + go get github.com/l3pp4rd/go-sqlmock + +## Use it with pleasure + +An example of some database interaction which you want to test: + +``` go +package main + +import ( + "database/sql" + _ "github.com/go-sql-driver/mysql" + "github.com/kisielk/sqlstruct" + "fmt" + "log" +) + +const ORDER_PENDING = 0 +const ORDER_CANCELLED = 1 + +type User struct { + Id int `sql:"id"` + Username string `sql:"username"` + Balance float64 `sql:"balance"` +} + +type Order struct { + Id int `sql:"id"` + Value float64 `sql:"value"` + ReservedFee float64 `sql:"reserved_fee"` + Status int `sql:"status"` +} + +func cancelOrder(id int, db *sql.DB) (err error) { + tx, err := db.Begin() + if err != nil { + return + } + + var order Order + var user User + sql := fmt.Sprintf(` +SELECT %s, %s +FROM orders AS o +INNER JOIN users AS u ON o.buyer_id = u.id +WHERE o.id = ? +FOR UPDATE`, + sqlstruct.ColumnsAliased(order, "o"), + sqlstruct.ColumnsAliased(user, "u")) + + // fetch order to cancel + rows, err := tx.Query(sql, id) + if err != nil { + tx.Rollback() + return + } + + defer rows.Close() + // no rows, nothing to do + if !rows.Next() { + tx.Rollback() + return + } + + // read order + err = sqlstruct.ScanAliased(&order, rows, "o") + if err != nil { + tx.Rollback() + return + } + + // ensure order status + if order.Status != ORDER_PENDING { + tx.Rollback() + return + } + + // read user + err = sqlstruct.ScanAliased(&user, rows, "u") + if err != nil { + tx.Rollback() + return + } + rows.Close() // manually close before other prepared statements + + // refund order value + sql = "UPDATE users SET balance = balance + ? WHERE id = ?" + refundStmt, err := tx.Prepare(sql) + if err != nil { + tx.Rollback() + return + } + defer refundStmt.Close() + _, err = refundStmt.Exec(order.Value + order.ReservedFee, user.Id) + if err != nil { + tx.Rollback() + return + } + + // update order status + order.Status = ORDER_CANCELLED + sql = "UPDATE orders SET status = ?, updated = NOW() WHERE id = ?" + orderUpdStmt, err := tx.Prepare(sql) + if err != nil { + tx.Rollback() + return + } + defer orderUpdStmt.Close() + _, err = orderUpdStmt.Exec(order.Status, order.Id) + if err != nil { + tx.Rollback() + return + } + return tx.Commit() +} + +func main() { + db, err := sql.Open("mysql", "root:nimda@/test") + if err != nil { + log.Fatal(err) + } + defer db.Close() + err = cancelOrder(1, db) + if err != nil { + log.Fatal(err) + } +} +``` + +And the clean nice test: + +``` go +package main + +import ( + "database/sql" + "github.com/l3pp4rd/go-sqlmock" + "testing" + "fmt" +) + +// will test that order with a different status, cannot be cancelled +func TestShouldNotCancelOrderWithNonPendingStatus(t *testing.T) { + // open database stub + db, err := sql.Open("mock", "") + if err != nil { + t.Errorf("An error '%s' was not expected when opening a stub database connection", err) + } + + // columns are prefixed with "o" since we used sqlstruct to generate them + columns := []string{"o_id", "o_status"} + // expect transaction begin + sqlmock.ExpectBegin() + // expect query to fetch order and user, match it with regexp + sqlmock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE"). + WithArgs(1). + WillReturnRows(sqlmock.RowsFromCSVString(columns, "1,1")) + // expect transaction rollback, since order status is "cancelled" + sqlmock.ExpectRollback() + + // run the cancel order function + err = cancelOrder(1, db) + if err != nil { + t.Errorf("Expected no error, but got %s instead", err) + } + // db.Close() ensures that all expectations have been met + if err = db.Close(); err != nil { + t.Errorf("Error '%s' was not expected while closing the database", err) + } +} + +// will test order cancellation +func TestShouldRefundUserWhenOrderIsCancelled(t *testing.T) { + // open database stub + db, err := sql.Open("mock", "") + if err != nil { + t.Errorf("An error '%s' was not expected when opening a stub database connection", err) + } + + // columns are prefixed with "o" since we used sqlstruct to generate them + columns := []string{"o_id", "o_status", "o_value", "o_reserved_fee", "u_id", "u_balance"} + // expect transaction begin + sqlmock.ExpectBegin() + // expect query to fetch order and user, match it with regexp + sqlmock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE"). + WithArgs(1). + WillReturnRows(sqlmock.RowsFromCSVString(columns, "1,0,25.75,3.25,2,10.00")) + // expect user balance update + sqlmock.ExpectExec("UPDATE users SET balance"). + WithArgs(25.75 + 3.25, 2). // refund amount, user id + WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row + // expect order status update + sqlmock.ExpectExec("UPDATE orders SET status"). + WithArgs(ORDER_CANCELLED, 1). // status, id + WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row + // expect a transaction commit + sqlmock.ExpectCommit() + + // run the cancel order function + err = cancelOrder(1, db) + if err != nil { + t.Errorf("Expected no error, but got %s instead", err) + } + // db.Close() ensures that all expectations have been met + if err = db.Close(); err != nil { + t.Errorf("Error '%s' was not expected while closing the database", err) + } +} + +// will test order cancellation +func TestShouldRollbackOnError(t *testing.T) { + // open database stub + db, err := sql.Open("mock", "") + if err != nil { + t.Errorf("An error '%s' was not expected when opening a stub database connection", err) + } + + // expect transaction begin + sqlmock.ExpectBegin() + // expect query to fetch order and user, match it with regexp + sqlmock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE"). + WithArgs(1). + WillReturnError(fmt.Errorf("Some error")) + // should rollback since error was returned from query execution + sqlmock.ExpectRollback() + + // run the cancel order function + err = cancelOrder(1, db) + // error should return back + if err == nil { + t.Error("Expected error, but got none") + } + // db.Close() ensures that all expectations have been met + if err = db.Close(); err != nil { + t.Errorf("Error '%s' was not expected while closing the database", err) + } +} +``` + +## Expectations + +All **Expect** methods return a **Mock** interface which allow you to describe +expectations in more details: return an error, expect specific arguments, return rows and so on. +**NOTE:** that if you call **WithArgs** on a non query based expectation, it will panic + +A **Mock** interface: + +``` go +type Mock interface { + WithArgs(...driver.Value) Mock + WillReturnError(error) Mock + WillReturnRows(driver.Rows) Mock + WillReturnResult(driver.Result) Mock +} +``` + +As an example we can expect a transaction commit and simulate an error for it: + +``` go +sqlmock.ExpectCommit().WillReturnError(fmt.Errorf("Deadlock occured")) +``` + +In same fashion, we can expect queries to match arguments. If there are any, it must be matched. +Instead of result we can return error.. + +``` go +sqlmock.ExpectQuery("SELECT (.*) FROM orders"). + WithArgs("string value"). + WillReturnResult(sqlmock.NewResult(0, 1)) +``` + +**WithArgs** expectation, compares values based on their type, for usual values like **string, float, int** +it matches the actual value. Types like **time** are compared only by type. Other types might require different ways +to compare them correctly, this may be improved. + +## Run tests + + go test + +## TODO + +- export to godoc +- handle argument comparison more efficiently + +## Contributions + +Feel free to open a pull request. + +## License + +The [three clause BSD license](http://en.wikipedia.org/wiki/BSD_licenses) diff --git a/expectations.go b/expectations.go index 7d1daf0..38567b4 100644 --- a/expectations.go +++ b/expectations.go @@ -41,9 +41,9 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) bool { if len(args) != len(e.args) { return false } - for k, v := range e.args { + for k, v := range args { vi := reflect.ValueOf(v) - ai := reflect.ValueOf(args[k]) + 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() { diff --git a/rows.go b/rows.go index 55502a2..f6a626c 100644 --- a/rows.go +++ b/rows.go @@ -54,7 +54,7 @@ func RowsFromCSVString(columns []string, s string) driver.Rows { row := make([]driver.Value, len(columns)) for i, v := range r { v := strings.TrimSpace(v) - row[i] = v + row[i] = []byte(v) } rs.rows = append(rs.rows, row) } diff --git a/sqlmock.go b/sqlmock.go index e22505d..70364d3 100644 --- a/sqlmock.go +++ b/sqlmock.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "regexp" + "strings" ) var mock *mockDriver @@ -35,10 +36,17 @@ type conn struct { active expectation } +func stripQuery(q string) (s string) { + s = strings.Replace(q, "\n", " ", -1) + s = strings.Replace(s, "\r", "", -1) + s = strings.TrimSpace(s) + return +} + func (c *conn) Close() (err error) { for _, e := range mock.conn.expectations { if !e.fulfilled() { - err = errors.New(fmt.Sprintf("There is expectation %+v which was not matched yet", e)) + err = errors.New(fmt.Sprintf("There is a remaining expectation %T which was not matched yet", e)) break } } @@ -81,7 +89,7 @@ func (c *conn) Begin() (driver.Tx, error) { etb, ok := e.(*expectedBegin) if !ok { - return nil, errors.New(fmt.Sprintf("Call to Begin transaction, was not expected, next expectation is %v", e)) + return nil, errors.New(fmt.Sprintf("Call to Begin transaction, was not expected, next expectation is %+v", e)) } etb.triggered = true return &transaction{c}, etb.err @@ -99,13 +107,14 @@ func (c *conn) next() (e expectation) { func (c *conn) Exec(query string, args []driver.Value) (driver.Result, error) { e := c.next() + query = stripQuery(query) if e == nil { - return nil, errors.New(fmt.Sprintf("All expectations were already fulfilled, call to Exec '%s' query with args [%v] was not expected", query, args)) + return nil, errors.New(fmt.Sprintf("All expectations were already fulfilled, call to Exec '%s' query with args %+v was not expected", query, args)) } eq, ok := e.(*expectedExec) if !ok { - return nil, errors.New(fmt.Sprintf("Call to Exec query '%s' with args [%v], was not expected, next expectation is %v", query, args, e)) + return nil, errors.New(fmt.Sprintf("Call to Exec query '%s' with args %+v, was not expected, next expectation is %+v", query, args, e)) } eq.triggered = true @@ -114,15 +123,15 @@ func (c *conn) Exec(query string, args []driver.Value) (driver.Result, error) { } if eq.result == nil { - return nil, errors.New(fmt.Sprintf("Exec query '%s' with args [%v], must return a database/sql/driver.Result, but it was not set for expectation %v", query, args, eq)) + return nil, errors.New(fmt.Sprintf("Exec query '%s' with args %+v, must return a database/sql/driver.Result, but it was not set for expectation %+v", query, args, eq)) } if !eq.queryMatches(query) { - return nil, errors.New(fmt.Sprintf("Exec query '%s', does not match regex [%s]", query, eq.sqlRegex.String())) + return nil, errors.New(fmt.Sprintf("Exec query '%s', does not match regex '%s'", query, eq.sqlRegex.String())) } if !eq.argsMatches(args) { - return nil, errors.New(fmt.Sprintf("Exec query '%s', args [%v] does not match expected [%v]", query, args, eq.args)) + return nil, errors.New(fmt.Sprintf("Exec query '%s', args %+v does not match expected %+v", query, args, eq.args)) } return eq.result, nil @@ -162,7 +171,7 @@ func (c *conn) WithArgs(args ...driver.Value) Mock { func (c *conn) WillReturnResult(result driver.Result) Mock { eq, ok := c.active.(*expectedExec) if !ok { - panic(fmt.Sprintf("driver.Result may be returned only by Exec expectations, current is %v", c.active)) + panic(fmt.Sprintf("driver.Result may be returned only by Exec expectations, current is %+v", c.active)) } eq.result = result return c @@ -171,25 +180,26 @@ func (c *conn) WillReturnResult(result driver.Result) Mock { func (c *conn) WillReturnRows(rows driver.Rows) Mock { eq, ok := c.active.(*expectedQuery) if !ok { - panic(fmt.Sprintf("driver.Rows may be returned only by Query expectations, current is %v", c.active)) + panic(fmt.Sprintf("driver.Rows may be returned only by Query expectations, current is %+v", c.active)) } eq.rows = rows return c } func (c *conn) Prepare(query string) (driver.Stmt, error) { - return &statement{c, query}, nil + return &statement{mock.conn, stripQuery(query)}, nil } func (c *conn) Query(query string, args []driver.Value) (driver.Rows, error) { e := c.next() + query = stripQuery(query) if e == nil { - return nil, errors.New(fmt.Sprintf("All expectations were already fulfilled, call to Query '%s' with args [%v] was not expected", query, args)) + return nil, errors.New(fmt.Sprintf("All expectations were already fulfilled, call to Query '%s' with args %+v was not expected", query, args)) } eq, ok := e.(*expectedQuery) if !ok { - return nil, errors.New(fmt.Sprintf("Call to Query '%s' with args [%v], was not expected, next expectation is %v", query, args, e)) + return nil, errors.New(fmt.Sprintf("Call to Query '%s' with args %+v, was not expected, next expectation is %+v", query, args, e)) } eq.triggered = true @@ -198,7 +208,7 @@ func (c *conn) Query(query string, args []driver.Value) (driver.Rows, error) { } if eq.rows == nil { - return nil, errors.New(fmt.Sprintf("Query '%s' with args [%v], must return a database/sql/driver.Rows, but it was not set for expectation %v", query, args, eq)) + return nil, errors.New(fmt.Sprintf("Query '%s' with args %+v, must return a database/sql/driver.Rows, but it was not set for expectation %+v", query, args, eq)) } if !eq.queryMatches(query) { @@ -206,7 +216,7 @@ func (c *conn) Query(query string, args []driver.Value) (driver.Rows, error) { } if !eq.argsMatches(args) { - return nil, errors.New(fmt.Sprintf("Query '%s', args [%v] does not match expected [%v]", query, args, eq.args)) + return nil, errors.New(fmt.Sprintf("Query '%s', args %+v does not match expected %+v", query, args, eq.args)) } return eq.rows, nil diff --git a/statement.go b/statement.go index d862b23..1e4af08 100644 --- a/statement.go +++ b/statement.go @@ -10,7 +10,6 @@ type statement struct { } func (stmt *statement) Close() error { - stmt.conn = nil return nil }