mirror of
https://github.com/zhashkevych/go-sqlxmock.git
synced 2025-02-16 18:34:27 +02:00
update readme and add example
* dcca987 add an old example and a basic one
This commit is contained in:
parent
5c18417f3f
commit
32a1b9d93f
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
|
||||
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.
|
||||
|
||||
If you need an old version, checkout **go-sqlmock** at gopkg.in:
|
||||
|
||||
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
|
||||
|
||||
go get github.com/DATA-DOG/go-sqlmock
|
||||
go get gopkg.in/DATA-DOG/go-sqlmock.v1
|
||||
|
||||
Or take an older version:
|
||||
|
||||
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
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/kisielk/sqlstruct"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
import "database/sql"
|
||||
|
||||
const ORDER_PENDING = 0
|
||||
const ORDER_CANCELLED = 1
|
||||
func recordStats(db *sql.DB, userID, productID int64) (err error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id int `sql:"id"`
|
||||
Username string `sql:"username"`
|
||||
Balance float64 `sql:"balance"`
|
||||
}
|
||||
defer func() {
|
||||
switch err {
|
||||
case nil:
|
||||
err = tx.Commit()
|
||||
default:
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
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()
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
// @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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And the clean nice test:
|
||||
### Tests with sqlmock
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"testing"
|
||||
"fmt"
|
||||
"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, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Errorf("An error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
// 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()
|
||||
|
||||
// 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.NewRows(columns).FromCSVString("1,1"))
|
||||
// expect transaction rollback, since order status is "cancelled"
|
||||
sqlmock.ExpectRollback()
|
||||
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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// will test order cancellation
|
||||
func TestShouldRefundUserWhenOrderIsCancelled(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)
|
||||
}
|
||||
// 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()
|
||||
|
||||
// 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.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()
|
||||
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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
go test
|
||||
|
||||
## 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.
|
||||
go test -race
|
||||
|
||||
## 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-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)
|
||||
|
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.
|
||||
|
||||
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
|
||||
are also supported.
|
||||
|
@ -617,3 +617,44 @@ func TestGoroutineExecutionWithUnorderedExpectationMatching(t *testing.T) {
|
||||
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:
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user