From 32a1b9d93f8906a6a3905ff5532edded050a18e2 Mon Sep 17 00:00:00 2001 From: gedi Date: Thu, 27 Aug 2015 10:34:01 +0300 Subject: [PATCH] update readme and add example * dcca987 add an old example and a basic one --- .gitignore | 4 +- README.md | 388 ++++++++------------------------- examples/basic/basic.go | 40 ++++ examples/basic/basic_test.go | 58 +++++ examples/blog/blog.go | 81 +++++++ examples/blog/blog_test.go | 102 +++++++++ examples/doc.go | 1 + examples/orders/orders.go | 121 ++++++++++ examples/orders/orders_test.go | 108 +++++++++ sqlmock.go | 3 +- sqlmock_test.go | 41 ++++ 11 files changed, 653 insertions(+), 294 deletions(-) create mode 100644 examples/basic/basic.go create mode 100644 examples/basic/basic_test.go create mode 100644 examples/blog/blog.go create mode 100644 examples/blog/blog_test.go create mode 100644 examples/doc.go create mode 100644 examples/orders/orders.go create mode 100644 examples/orders/orders_test.go diff --git a/.gitignore b/.gitignore index 8493d1d..e4001c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/*.test +/examples/blog/blog +/examples/orders/orders +/examples/basic/basic diff --git a/README.md b/README.md index c43b8ca..8cbda93 100644 --- a/README.md +++ b/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) diff --git a/examples/basic/basic.go b/examples/basic/basic.go new file mode 100644 index 0000000..0fbf98d --- /dev/null +++ b/examples/basic/basic.go @@ -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) + } +} diff --git a/examples/basic/basic_test.go b/examples/basic/basic_test.go new file mode 100644 index 0000000..0825f90 --- /dev/null +++ b/examples/basic/basic_test.go @@ -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) + } +} diff --git a/examples/blog/blog.go b/examples/blog/blog.go new file mode 100644 index 0000000..c4aec06 --- /dev/null +++ b/examples/blog/blog.go @@ -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) +} diff --git a/examples/blog/blog_test.go b/examples/blog/blog_test.go new file mode 100644 index 0000000..aa1881a --- /dev/null +++ b/examples/blog/blog_test.go @@ -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) + } +} diff --git a/examples/doc.go b/examples/doc.go new file mode 100644 index 0000000..c7842af --- /dev/null +++ b/examples/doc.go @@ -0,0 +1 @@ +package examples diff --git a/examples/orders/orders.go b/examples/orders/orders.go new file mode 100644 index 0000000..fb7e47e --- /dev/null +++ b/examples/orders/orders.go @@ -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) + } +} diff --git a/examples/orders/orders_test.go b/examples/orders/orders_test.go new file mode 100644 index 0000000..7562b8f --- /dev/null +++ b/examples/orders/orders_test.go @@ -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) + } +} diff --git a/sqlmock.go b/sqlmock.go index 605575b..7ac5dbb 100644 --- a/sqlmock.go +++ b/sqlmock.go @@ -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. diff --git a/sqlmock_test.go b/sqlmock_test.go index deb37ff..1fb7af1 100644 --- a/sqlmock_test.go +++ b/sqlmock_test.go @@ -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: +}