You've already forked go-sqlxmock
mirror of
https://github.com/zhashkevych/go-sqlxmock.git
synced 2025-06-12 21:47:29 +02:00
update readme and add example
* dcca987 add an old example and a basic one
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
/*.test
|
/examples/blog/blog
|
||||||
|
/examples/orders/orders
|
||||||
|
/examples/basic/basic
|
||||||
|
388
README.md
388
README.md
@ -7,347 +7,151 @@ This is a **mock** driver as **database/sql/driver** which is very flexible and
|
|||||||
manage and mock expected queries. All the expectations should be met and all queries and actions
|
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. The package has no 3rd party dependencies.
|
triggered should be mocked in order to pass a test. The package has no 3rd party dependencies.
|
||||||
|
|
||||||
**NOTE:** regarding major issues #20 and #9 the api has changed to support concurrency and more than
|
**NOTE:** regarding major issues #20 and #9 the **api** has changed to support concurrency and more than
|
||||||
one database connection.
|
one database connection.
|
||||||
|
|
||||||
If you need an old version, checkout **go-sqlmock** at gopkg.in:
|
If you need an old version, checkout **go-sqlmock** at gopkg.in:
|
||||||
|
|
||||||
go get gopkg.in/DATA-DOG/go-sqlmock.v0
|
go get gopkg.in/DATA-DOG/go-sqlmock.v0
|
||||||
|
|
||||||
Otherwise use the **v1** branch from master.
|
Otherwise use the **v1** branch from master which should be stable afterwards, because all the issues which
|
||||||
|
were known will be fixed in this version.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
go get github.com/DATA-DOG/go-sqlmock
|
go get gopkg.in/DATA-DOG/go-sqlmock.v1
|
||||||
|
|
||||||
Or take an older version:
|
Or take an older version:
|
||||||
|
|
||||||
go get gopkg.in/DATA-DOG/go-sqlmock.v0
|
go get gopkg.in/DATA-DOG/go-sqlmock.v0
|
||||||
|
|
||||||
## Use it with pleasure
|
## Documentation and Examples
|
||||||
|
|
||||||
An example of some database interaction which you may want to test:
|
Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference.
|
||||||
|
See **.travis.yml** for supported **go** versions.
|
||||||
|
Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb)
|
||||||
|
all database related actions are isolated within a single transaction so the database can remain in the same state.
|
||||||
|
|
||||||
|
See implementation examples:
|
||||||
|
|
||||||
|
- [blog API server](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/blog)
|
||||||
|
- [the same orders example](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/orders)
|
||||||
|
|
||||||
|
### Something you may want to test
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "database/sql"
|
||||||
"database/sql"
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
"github.com/kisielk/sqlstruct"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ORDER_PENDING = 0
|
func recordStats(db *sql.DB, userID, productID int64) (err error) {
|
||||||
const ORDER_CANCELLED = 1
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
defer func() {
|
||||||
Id int `sql:"id"`
|
switch err {
|
||||||
Username string `sql:"username"`
|
case nil:
|
||||||
Balance float64 `sql:"balance"`
|
err = tx.Commit()
|
||||||
}
|
default:
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
type Order struct {
|
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
|
||||||
Id int `sql:"id"`
|
return
|
||||||
Value float64 `sql:"value"`
|
}
|
||||||
ReservedFee float64 `sql:"reserved_fee"`
|
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
|
||||||
Status int `sql:"status"`
|
return
|
||||||
}
|
}
|
||||||
|
return
|
||||||
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() {
|
func main() {
|
||||||
db, err := sql.Open("mysql", "root:nimda@/test")
|
// @NOTE: the real connection is not required for tests
|
||||||
if err != nil {
|
db, err := sql.Open("mysql", "root@/blog")
|
||||||
log.Fatal(err)
|
if err != nil {
|
||||||
}
|
panic(err)
|
||||||
defer db.Close()
|
}
|
||||||
err = cancelOrder(1, db)
|
defer db.Close()
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
|
||||||
}
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
And the clean nice test:
|
### Tests with sqlmock
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"testing"
|
||||||
"testing"
|
|
||||||
"fmt"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// will test that order with a different status, cannot be cancelled
|
// a successful case
|
||||||
func TestShouldNotCancelOrderWithNonPendingStatus(t *testing.T) {
|
func TestShouldUpdateStats(t *testing.T) {
|
||||||
// open database stub
|
db, mock, err := sqlmock.New()
|
||||||
db, err := sqlmock.New()
|
if err != nil {
|
||||||
if err != nil {
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
}
|
||||||
}
|
defer db.Close()
|
||||||
|
|
||||||
// columns are prefixed with "o" since we used sqlstruct to generate them
|
mock.ExpectBegin()
|
||||||
columns := []string{"o_id", "o_status"}
|
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
// expect transaction begin
|
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
sqlmock.ExpectBegin()
|
mock.ExpectCommit()
|
||||||
// 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.NewRows(columns).FromCSVString("1,1"))
|
|
||||||
// expect transaction rollback, since order status is "cancelled"
|
|
||||||
sqlmock.ExpectRollback()
|
|
||||||
|
|
||||||
// run the cancel order function
|
// now we execute our method
|
||||||
err = cancelOrder(1, db)
|
if err = recordStats(db, 2, 3); err != nil {
|
||||||
if err != nil {
|
t.Errorf("error was not expected while updating stats: %s", err)
|
||||||
t.Errorf("Expected no error, but got %s instead", err)
|
}
|
||||||
}
|
|
||||||
// db.Close() ensures that all expectations have been met
|
// we make sure that all expectations were met
|
||||||
if err = db.Close(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("Error '%s' was not expected while closing the database", err)
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// will test order cancellation
|
// a failing test case
|
||||||
func TestShouldRefundUserWhenOrderIsCancelled(t *testing.T) {
|
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
|
||||||
// open database stub
|
db, mock, err := sqlmock.New()
|
||||||
db, err := sqlmock.New()
|
if err != nil {
|
||||||
if err != nil {
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
}
|
||||||
}
|
defer db.Close()
|
||||||
|
|
||||||
// columns are prefixed with "o" since we used sqlstruct to generate them
|
mock.ExpectBegin()
|
||||||
columns := []string{"o_id", "o_status", "o_value", "o_reserved_fee", "u_id", "u_balance"}
|
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
// expect transaction begin
|
mock.ExpectExec("INSERT INTO product_viewers").
|
||||||
sqlmock.ExpectBegin()
|
WithArgs(2, 3).
|
||||||
// expect query to fetch order and user, match it with regexp
|
WillReturnError(fmt.Errorf("some error"))
|
||||||
sqlmock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE").
|
mock.ExpectRollback()
|
||||||
WithArgs(1).
|
|
||||||
WillReturnRows(sqlmock.NewRows(columns).AddRow(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
|
// now we execute our method
|
||||||
err = cancelOrder(1, db)
|
if err = recordStats(db, 2, 3); err == nil {
|
||||||
if err != nil {
|
t.Errorf("was expecting an error, but there was none")
|
||||||
t.Errorf("Expected no error, but got %s instead", err)
|
}
|
||||||
}
|
|
||||||
// db.Close() ensures that all expectations have been met
|
// we make sure that all expectations were met
|
||||||
if err = db.Close(); err != nil {
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
t.Errorf("Error '%s' was not expected while closing the database", err)
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// will test order cancellation
|
|
||||||
func TestShouldRollbackOnError(t *testing.T) {
|
|
||||||
// open database stub
|
|
||||||
db, err := sqlmock.New()
|
|
||||||
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").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"col"}).AddRow("val"))
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTE:** it matches a regular expression. Some regex special characters must be escaped if you want to match them.
|
|
||||||
For example if we want to match a subselect:
|
|
||||||
|
|
||||||
``` go
|
|
||||||
sqlmock.ExpectQuery("SELECT (.*) FROM orders WHERE id IN \\(SELECT id FROM finished WHERE status = 1\\)").
|
|
||||||
WithArgs("string value").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"col"}).AddRow("val"))
|
|
||||||
```
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
You can build rows either from CSV string or from interface values:
|
|
||||||
|
|
||||||
**Rows** interface, which satisfies sql driver.Rows:
|
|
||||||
|
|
||||||
``` go
|
|
||||||
type Rows interface {
|
|
||||||
AddRow(...driver.Value) Rows
|
|
||||||
FromCSVString(s string) Rows
|
|
||||||
Next([]driver.Value) error
|
|
||||||
Columns() []string
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Example for to build rows:
|
|
||||||
|
|
||||||
``` go
|
|
||||||
rs := sqlmock.NewRows([]string{"column1", "column2"}).
|
|
||||||
FromCSVString("one,1\ntwo,2").
|
|
||||||
AddRow("three", 3)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Prepare** will ignore other expectations if ExpectPrepare not set. When set, can expect normal result or simulate an error:
|
|
||||||
|
|
||||||
``` go
|
|
||||||
rs := sqlmock.ExpectPrepare().
|
|
||||||
WillReturnError(fmt.Errorf("Query prepare failed"))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run tests
|
## Run tests
|
||||||
|
|
||||||
go test
|
go test -race
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock)
|
|
||||||
See **.travis.yml** for supported **go** versions
|
|
||||||
Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb)
|
|
||||||
all database related actions are isolated within a single transaction so the database can remain in the same state.
|
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
|
- **2015-08-27** - **v1** api change, concurrency support, all known issues fixed.
|
||||||
- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error
|
- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error
|
||||||
- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for
|
- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for
|
||||||
interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5)
|
interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5)
|
||||||
|
40
examples/basic/basic.go
Normal file
40
examples/basic/basic.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
func recordStats(db *sql.DB, userID, productID int64) (err error) {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
err = tx.Commit()
|
||||||
|
default:
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// @NOTE: the real connection is not required for tests
|
||||||
|
db, err := sql.Open("mysql", "root@/blog")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
58
examples/basic/basic_test.go
Normal file
58
examples/basic/basic_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a successful case
|
||||||
|
func TestShouldUpdateStats(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
// now we execute our method
|
||||||
|
if err = recordStats(db, 2, 3); err != nil {
|
||||||
|
t.Errorf("error was not expected while updating stats: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a failing test case
|
||||||
|
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
mock.ExpectExec("INSERT INTO product_viewers").
|
||||||
|
WithArgs(2, 3).
|
||||||
|
WillReturnError(fmt.Errorf("some error"))
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
// now we execute our method
|
||||||
|
if err = recordStats(db, 2, 3); err == nil {
|
||||||
|
t.Errorf("was expecting an error, but there was none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
81
examples/blog/blog.go
Normal file
81
examples/blog/blog.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type api struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type post struct {
|
||||||
|
ID int
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) posts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rows, err := a.db.Query("SELECT id, title, body FROM posts")
|
||||||
|
if err != nil {
|
||||||
|
a.fail(w, "failed to fetch posts: "+err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var posts []*post
|
||||||
|
for rows.Next() {
|
||||||
|
p := &post{}
|
||||||
|
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
|
||||||
|
a.fail(w, "failed to scan post: "+err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
posts = append(posts, p)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
a.fail(w, "failed to read all posts: "+rows.Err().Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Posts []*post
|
||||||
|
}{posts}
|
||||||
|
|
||||||
|
a.ok(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// @NOTE: the real connection is not required for tests
|
||||||
|
db, err := sql.Open("mysql", "root@/blog")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
app := &api{db: db}
|
||||||
|
http.HandleFunc("/posts", app.posts)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) fail(w http.ResponseWriter, msg string, status int) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Error string
|
||||||
|
}{Error: msg}
|
||||||
|
|
||||||
|
resp, _ := json.Marshal(data)
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) ok(w http.ResponseWriter, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
a.fail(w, "oops something evil has happened", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(resp)
|
||||||
|
}
|
102
examples/blog/blog_test.go
Normal file
102
examples/blog/blog_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *api) assertJSON(actual []byte, data interface{}, t *testing.T) {
|
||||||
|
expected, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected when marshaling expected json data", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Compare(expected, actual) != 0 {
|
||||||
|
t.Errorf("the expected json: %s is different from actual %s", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldGetPosts(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// create app with mocked db, request and response to test
|
||||||
|
app := &api{db}
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost/posts", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected while creating request", err)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// before we actually execute our api function, we need to expect required DB actions
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "title", "body"}).
|
||||||
|
AddRow(1, "post 1", "hello").
|
||||||
|
AddRow(2, "post 2", "world")
|
||||||
|
|
||||||
|
mock.ExpectQuery("^SELECT (.+) FROM posts$").WillReturnRows(rows)
|
||||||
|
|
||||||
|
// now we execute our request
|
||||||
|
app.posts(w, req)
|
||||||
|
|
||||||
|
if w.Code != 200 {
|
||||||
|
t.Fatalf("expected status code to be 200, but got: %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Posts []*post
|
||||||
|
}{Posts: []*post{
|
||||||
|
{ID: 1, Title: "post 1", Body: "hello"},
|
||||||
|
{ID: 2, Title: "post 2", Body: "world"},
|
||||||
|
}}
|
||||||
|
app.assertJSON(w.Body.Bytes(), data, t)
|
||||||
|
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRespondWithErrorOnFailure(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// create app with mocked db, request and response to test
|
||||||
|
app := &api{db}
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost/posts", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("an error '%s' was not expected while creating request", err)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// before we actually execute our api function, we need to expect required DB actions
|
||||||
|
mock.ExpectQuery("^SELECT (.+) FROM posts$").WillReturnError(fmt.Errorf("some error"))
|
||||||
|
|
||||||
|
// now we execute our request
|
||||||
|
app.posts(w, req)
|
||||||
|
|
||||||
|
if w.Code != 500 {
|
||||||
|
t.Fatalf("expected status code to be 500, but got: %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Error string
|
||||||
|
}{"failed to fetch posts: some error"}
|
||||||
|
app.assertJSON(w.Body.Bytes(), data, t)
|
||||||
|
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
1
examples/doc.go
Normal file
1
examples/doc.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package examples
|
121
examples/orders/orders.go
Normal file
121
examples/orders/orders.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/kisielk/sqlstruct"
|
||||||
|
)
|
||||||
|
|
||||||
|
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() {
|
||||||
|
// @NOTE: the real connection is not required for tests
|
||||||
|
db, err := sql.Open("mysql", "root:@/orders")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
err = cancelOrder(1, db)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
108
examples/orders/orders_test.go
Normal file
108
examples/orders/orders_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// will test that order with a different status, cannot be cancelled
|
||||||
|
func TestShouldNotCancelOrderWithNonPendingStatus(t *testing.T) {
|
||||||
|
// open database stub
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// columns are prefixed with "o" since we used sqlstruct to generate them
|
||||||
|
columns := []string{"o_id", "o_status"}
|
||||||
|
// expect transaction begin
|
||||||
|
mock.ExpectBegin()
|
||||||
|
// expect query to fetch order and user, match it with regexp
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE").
|
||||||
|
WithArgs(1).
|
||||||
|
WillReturnRows(sqlmock.NewRows(columns).FromCSVString("1,1"))
|
||||||
|
// expect transaction rollback, since order status is "cancelled"
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
// run the cancel order function
|
||||||
|
err = cancelOrder(1, db)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got %s instead", err)
|
||||||
|
}
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will test order cancellation
|
||||||
|
func TestShouldRefundUserWhenOrderIsCancelled(t *testing.T) {
|
||||||
|
// open database stub
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
mock.ExpectBegin()
|
||||||
|
// expect query to fetch order and user, match it with regexp
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM orders AS o INNER JOIN users AS u (.+) FOR UPDATE").
|
||||||
|
WithArgs(1).
|
||||||
|
WillReturnRows(sqlmock.NewRows(columns).AddRow(1, 0, 25.75, 3.25, 2, 10.00))
|
||||||
|
// expect user balance update
|
||||||
|
mock.ExpectPrepare("UPDATE users SET balance").ExpectExec().
|
||||||
|
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
|
||||||
|
mock.ExpectPrepare("UPDATE orders SET status").ExpectExec().
|
||||||
|
WithArgs(ORDER_CANCELLED, 1). // status, id
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row
|
||||||
|
// expect a transaction commit
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
// run the cancel order function
|
||||||
|
err = cancelOrder(1, db)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got %s instead", err)
|
||||||
|
}
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// will test order cancellation
|
||||||
|
func TestShouldRollbackOnError(t *testing.T) {
|
||||||
|
// open database stub
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// expect transaction begin
|
||||||
|
mock.ExpectBegin()
|
||||||
|
// expect query to fetch order and user, match it with regexp
|
||||||
|
mock.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
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
// run the cancel order function
|
||||||
|
err = cancelOrder(1, db)
|
||||||
|
// error should return back
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, but got none")
|
||||||
|
}
|
||||||
|
// we make sure that all expectations were met
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,8 @@ Package sqlmock provides sql driver connection, which allows to test database
|
|||||||
interactions by expected calls and simulate their results or errors.
|
interactions by expected calls and simulate their results or errors.
|
||||||
|
|
||||||
It does not require any modifications to your source code in order to test
|
It does not require any modifications to your source code in order to test
|
||||||
and mock database operations.
|
and mock database operations. It does not even require a real database in order
|
||||||
|
to test your application.
|
||||||
|
|
||||||
The driver allows to mock any sql driver method behavior. Concurrent actions
|
The driver allows to mock any sql driver method behavior. Concurrent actions
|
||||||
are also supported.
|
are also supported.
|
||||||
|
@ -617,3 +617,44 @@ func TestGoroutineExecutionWithUnorderedExpectationMatching(t *testing.T) {
|
|||||||
t.Errorf("there were unfulfilled expections: %s", err)
|
t.Errorf("there were unfulfilled expections: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleSqlmock_goroutines() {
|
||||||
|
db, mock, err := New()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to open sqlmock database:", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// note this line is important for unordered expectation matching
|
||||||
|
mock.MatchExpectationsInOrder = false
|
||||||
|
|
||||||
|
result := NewResult(1, 1)
|
||||||
|
|
||||||
|
mock.ExpectExec("^UPDATE one").WithArgs("one").WillReturnResult(result)
|
||||||
|
mock.ExpectExec("^UPDATE two").WithArgs("one", "two").WillReturnResult(result)
|
||||||
|
mock.ExpectExec("^UPDATE three").WithArgs("one", "two", "three").WillReturnResult(result)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
queries := map[string][]interface{}{
|
||||||
|
"one": []interface{}{"one"},
|
||||||
|
"two": []interface{}{"one", "two"},
|
||||||
|
"three": []interface{}{"one", "two", "three"},
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(len(queries))
|
||||||
|
for table, args := range queries {
|
||||||
|
go func(tbl string, a []interface{}) {
|
||||||
|
if _, err := db.Exec("UPDATE "+tbl, a...); err != nil {
|
||||||
|
fmt.Println("error was not expected:", err)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(table, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
fmt.Println("there were unfulfilled expections:", err)
|
||||||
|
}
|
||||||
|
// Output:
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user