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:
parent
3e67393335
commit
2fc5a0dd15
28
LICENSE
Normal file
28
LICENSE
Normal 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
304
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)
|
||||
|
||||
|
@ -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() {
|
||||
|
2
rows.go
2
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)
|
||||
}
|
||||
|
38
sqlmock.go
38
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
|
||||
|
@ -10,7 +10,6 @@ type statement struct {
|
||||
}
|
||||
|
||||
func (stmt *statement) Close() error {
|
||||
stmt.conn = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user