1
0
mirror of https://github.com/zhashkevych/go-sqlxmock.git synced 2024-11-24 08:12:13 +02:00

add readme and update error messages

This commit is contained in:
gedi 2014-02-06 17:03:46 +02:00
parent 3e67393335
commit 2fc5a0dd15
6 changed files with 352 additions and 25 deletions

28
LICENSE Normal file
View File

@ -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.

304
README.md
View File

@ -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)

View File

@ -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() {

View File

@ -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)
}

View File

@ -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

View File

@ -10,7 +10,6 @@ type statement struct {
}
func (stmt *statement) Close() error {
stmt.conn = nil
return nil
}