You've already forked go-sqlmock
mirror of
https://github.com/DATA-DOG/go-sqlmock.git
synced 2026-05-22 09:55:22 +02:00
update readme and add example
* dcca987 add an old example and a basic one
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package examples
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user