First commit.

This commit is contained in:
Dmitriy Seredenko
2017-06-02 19:23:35 +02:00
parent d2155f1e30
commit fd81ce44f3
8 changed files with 703 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
+84
View File
@@ -0,0 +1,84 @@
###Go-Mocket
Go-Mocket is library inspired by [DATA-DOG/go-sqlmock](https://github.com/DATA-DOG/go-sqlmock)
As inspiration library it is implementation of [sql/driver](https://godoc.org/database/sql/driver) interface but at the same time follows different approaches and has only similar API.
This library helps to mock any DB connection also with [jinzhu/gorm](https://github.com/jinzhu/gorm) and it was main goal to create it
List of features in the library:
* Mock `INSERT`, `UPDATE`, `SELECT`, `DELETE`
* Support of transactions
* 2 API's to use - `chaining` and via specifying whole mock object
* Matching by prepared statements arguments
* You will not require to change anything inside you code to start using this library
* Ability to trigger exceptions
* Attach callbacks to mocked response to add additional check or modify response
**NOTE** Please be aware that driver catches SQL without DB specifics. Generating of queries is done by *sql* package
####Install
```
go get github.com/selvatico/go-mocket
```
#### Usage
There are two possible ways to use `mocket`:
* Chaining API
* Specifying `FakeResponse` object with all fields manually. Could be useful for
##### Enabling driver
Somewhere in you code to setup a tests
```
import (
"database/sql"
mocket "github.com/selvatico/go-mocket"
"github.com/jinzhu/gorm"
)
func SetupTests() {
sql.Register("fake_test", mocket.FakeDriver{})
// GORM
db, err := gorm.Open("fake_test", "connection_string") // Could be any connection string
app.DB = db
// Regular sql package usage
db, err := sql.Open(driver, source)
}
```
Now if use singleton instance of DB it will use everywhere mocked connection.
##### Chain usage
###### Example of mocking by pattern
```go
import mocket "github.com/selvatico/go-mocket"
import "net/http/httptest"
func TestHandler(t *testing.T) {
request := httptest.NewRequest("POST", "/application", postBody)
recorder := httptest.NewRecorder()
GlobalMock := mocket.Catcher
GlobalMock.Logging = true // log mocket behavior
commonReply := []map[string]interface{}{{"id": "2", "field": "value"}}
// Mock only by query pattern
GlobalMock.NewMock().WithQuery(`"campaigns".name IS NULL AND (("uuid" = test_uuid))`).WithReply(commonReply)
Post(recorder, request) // call handler
r := recorder.Result()
body, _ := ioutil.ReadAll(r.Body)
// some assertion about results
//...
}
```
Documentation in progress....
+66
View File
@@ -0,0 +1,66 @@
package go_mocket
import (
"context"
"database/sql/driver"
"errors"
"strings"
"sync"
)
type FakeConn struct {
db *FakeDB
currTx *FakeTx // Transaction pointer
mu sync.Mutex
bad bool
}
func (c *FakeConn) isBad() bool {
return false
}
func (c *FakeConn) Begin() (driver.Tx, error) {
if c.isBad() {
return nil, driver.ErrBadConn
}
if c.currTx != nil {
return nil, errors.New("already in a transaction")
}
c.currTx = &FakeTx{c: c}
return c.currTx, nil
}
func (c *FakeConn) Close() (err error) {
c.db = nil
return nil
}
func (c *FakeConn) Exec(query string, args []driver.Value) (driver.Result, error) {
panic("ExecContext was not called.")
}
func (c *FakeConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
return nil, driver.ErrSkip
}
func (c *FakeConn) Query(query string, args []driver.Value) (driver.Rows, error) {
panic("QueryContext was not called.")
}
// We do
func (c *FakeConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
return nil, driver.ErrSkip
}
// Should not be called
func (c *FakeConn) Prepare(query string) (driver.Stmt, error) {
panic("use Prepare")
}
func (c *FakeConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
var firstStmt = &FakeStmt{q: query, connection: c} // Create statement
firstStmt.placeholders = len(strings.Split(query, "?")) - 1 // Checking how many placeholders do we have
queryParts := strings.Split(query, " ") // By First statement define the query type
firstStmt.command = strings.ToUpper(queryParts[0])
return firstStmt, nil
}
+59
View File
@@ -0,0 +1,59 @@
package go_mocket
import (
"database/sql/driver"
"log"
"sync"
)
var _ = log.Printf
type FakeDriver struct {
mu sync.Mutex // guards 3 following fields
openCount int // conn opens
closeCount int // conn closes
waitCh chan struct{}
waitingCh chan struct{}
dbs map[string]*FakeDB
}
type FakeDB struct {
name string
mu sync.Mutex
tables map[string]*table
badConn bool
}
type table struct {
mu sync.Mutex
colname []string
coltype []string
rows []*row
}
func (t *table) columnIndex(name string) int {
for n, name := range t.colname {
if name == name {
return n
}
}
return -1
}
func (d FakeDriver) Open(database string) (driver.Conn, error) {
return &FakeConn{db: d.getDB(database)}, nil
}
func (d *FakeDriver) getDB(name string) *FakeDB {
d.mu.Lock()
defer d.mu.Unlock()
if d.dbs == nil {
d.dbs = make(map[string]*FakeDB)
}
db, ok := d.dbs[name]
if !ok {
db = &FakeDB{name: name}
d.dbs[name] = db
}
return db
}
+165
View File
@@ -0,0 +1,165 @@
package go_mocket
import (
"database/sql/driver"
"fmt"
"log"
"reflect"
"strings"
)
var Catcher *MockCatcher
type MockCatcher struct {
Mocks []*FakeResponse
Logging bool
PanicOnEmptyResponse bool // If not response matches - do we need to panic?
}
func (this *MockCatcher) Attach(fr []*FakeResponse) {
this.Mocks = append(this.Mocks, fr...)
}
// Find suitable response by provided
func (this *MockCatcher) FindResponse(query string, args []driver.NamedValue) *FakeResponse {
if this.Logging {
log.Printf("mock_catcher: check query: %s", query)
}
for _, resp := range this.Mocks {
if resp.IsMatch(query, args) {
resp.MarkAsTriggered()
return resp
}
}
if this.PanicOnEmptyResponse {
panic(fmt.Sprintf("No responses matches query %s ", query))
}
// Let's have always dummy version of response
return &FakeResponse{
Response: make([]map[string]interface{}, 0),
Exceptions: &Exceptions{},
}
}
// Create new FakeResponse and return for chains of attachments
func (this *MockCatcher) NewMock() *FakeResponse {
fr := &FakeResponse{Exceptions: &Exceptions{}, Response: make([]map[string]interface{}, 0)}
this.Mocks = append(this.Mocks, fr)
return fr
}
// Remove all Mocks to start process again
func (this *MockCatcher) Reset() *MockCatcher {
this.Mocks = make([]*FakeResponse, 0)
return this
}
// Possible exceptions during query executions
type Exceptions struct {
HookQueryBadConnection func() bool
HookExecBadConnection func() bool
}
// Represents mock of response with holding all required values to return mocked response
type FakeResponse struct {
Pattern string // SQL query pattern to match with
Args []interface{} // List args to be matched with
Response []map[string]interface{} // Array of rows to be parsed as result
Once bool // To trigger only once
Triggered bool // If it was triggered at least once
Callback func(string, []driver.NamedValue) // Callback to execute when response triggered
RowsAffected int64 // Defines affected rows count
LastInsertId int64 // ID to be returned for INSERT queries
*Exceptions
}
// Return true either when nothing to compare or deep equal check passed
func (fr *FakeResponse) isArgsMatch(args []driver.NamedValue) bool {
arguments := make([]interface{}, len(args))
if len(args) > 0 {
for index, arg := range args {
arguments[index] = arg.Value
}
}
return fr.Args == nil || reflect.DeepEqual(fr.Args, arguments)
}
func (fr *FakeResponse) isQueryMatch(query string) bool {
return fr.Pattern == "" || strings.Contains(query, fr.Pattern)
}
func (fr *FakeResponse) IsMatch(query string, args []driver.NamedValue) bool {
if fr.Once && fr.Triggered {
return false
}
return fr.isQueryMatch(query) && fr.isArgsMatch(args)
}
func (fr *FakeResponse) MarkAsTriggered() {
fr.Triggered = true
}
// For chaining init
func (fr *FakeResponse) WithQuery(query string) *FakeResponse {
fr.Pattern = query
return fr
}
// Attach Args check for prepared statements
func (fr *FakeResponse) WithArgs(vars ...interface{}) *FakeResponse {
if len(vars) > 0 {
fr.Args = make([]interface{}, len(vars))
for index, v := range vars {
fr.Args[index] = v
}
}
return fr
}
// Methods to chain and assign some parts of response
func (fr *FakeResponse) WithReply(response []map[string]interface{}) *FakeResponse {
fr.Response = response
return fr
}
func (fr *FakeResponse) OneTime() *FakeResponse {
fr.Once = true
return fr
}
func (fr *FakeResponse) WithExecException() *FakeResponse {
fr.Exceptions.HookExecBadConnection = func() bool {
return true
}
return fr
}
func (fr *FakeResponse) WithQueryException() *FakeResponse {
fr.Exceptions.HookQueryBadConnection = func() bool {
return true
}
return fr
}
func (fr *FakeResponse) WithCallback(f func(string, []driver.NamedValue)) *FakeResponse {
fr.Callback = f
return fr
}
func (fr *FakeResponse) WithRowsNum(num int64) *FakeResponse {
fr.RowsAffected = num
return fr
}
func (fr *FakeResponse) WithId(id int64) *FakeResponse {
fr.LastInsertId = id
return fr
}
func init() {
Catcher = &MockCatcher{}
}
+22
View File
@@ -0,0 +1,22 @@
package go_mocket
import (
"database/sql/driver"
)
type FakeResult struct {
insertID int64
rowsAffected int64
}
func NewFakeResult(insertId int64, rowsAffected int64) driver.Result {
return &FakeResult{insertId, rowsAffected}
}
func (fr *FakeResult) LastInsertId() (int64, error) {
return fr.insertID, nil
}
func (fr *FakeResult) RowsAffected() (int64, error) {
return fr.rowsAffected, nil
}
+115
View File
@@ -0,0 +1,115 @@
package go_mocket
import (
"database/sql"
"database/sql/driver"
"errors"
"io"
"reflect"
"time"
)
type RowsCursor struct {
cols []string
colType [][]string
posSet int
posRow int
rows [][]*row
closed bool
// errPos and err are for making Next return early with error.
errPos int
err error
bytesClone map[*byte][]byte
}
type row struct {
cols []interface{} // must be same size as its table colname + coltype
}
func (rc *RowsCursor) Close() error {
if !rc.closed {
for _, bs := range rc.bytesClone {
bs[0] = 255 // first byte corrupted
}
}
rc.closed = true
return nil
}
func (rc *RowsCursor) Columns() []string {
return rc.cols
}
func (rc *RowsCursor) ColumnTypeScanType(index int) reflect.Type {
return colTypeToReflectType(rc.colType[rc.posSet][index])
}
func (rc *RowsCursor) Next(accumulator []driver.Value) error {
if rc.closed {
return errors.New("fake_db_driver: cursor is closed")
}
rc.posRow++
if rc.posRow == rc.errPos {
return rc.err
}
if rc.posRow >= len(rc.rows[rc.posSet]) {
return io.EOF // per interface spec
}
for i, v := range rc.rows[rc.posSet][rc.posRow].cols {
accumulator[i] = v
if bs, ok := v.([]byte); ok {
if rc.bytesClone == nil {
rc.bytesClone = make(map[*byte][]byte)
}
clone, ok := rc.bytesClone[&bs[0]]
if !ok {
clone = make([]byte, len(bs))
copy(clone, bs)
rc.bytesClone[&bs[0]] = clone
}
accumulator[i] = clone
}
}
return nil
}
func (rc *RowsCursor) HasNextResultSet() bool {
return rc.posSet < len(rc.rows)-1
}
func (rc *RowsCursor) NextResultSet() error {
if rc.HasNextResultSet() {
rc.posSet++
rc.posRow = -1
return nil
}
return io.EOF // Per interface spec.
}
func colTypeToReflectType(typ string) reflect.Type {
switch typ {
case "bool":
return reflect.TypeOf(false)
case "nullbool":
return reflect.TypeOf(sql.NullBool{})
case "int32":
return reflect.TypeOf(int32(0))
case "string":
return reflect.TypeOf("")
case "nullstring":
return reflect.TypeOf(sql.NullString{})
case "int64":
return reflect.TypeOf(int64(0))
case "nullint64":
return reflect.TypeOf(sql.NullInt64{})
case "float64":
return reflect.TypeOf(float64(0))
case "nullfloat64":
return reflect.TypeOf(sql.NullFloat64{})
case "datetime":
return reflect.TypeOf(time.Time{})
}
panic("invalid fakedb column type of " + typ)
}
+175
View File
@@ -0,0 +1,175 @@
package go_mocket
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"math/rand"
"strings"
)
type FakeStmt struct {
connection *FakeConn
q string // just for debugging SQL query generated by sql package
command string // String name of the command SELECT etc, taken as first word in the query
next *FakeStmt // used for returning multiple results.
closed bool // If connection closed already
colName []string //Names of columns in response
colType []string // Not used for now
placeholders int // Amount of passed args
}
func (s *FakeStmt) ColumnConverter(idx int) driver.ValueConverter {
return driver.DefaultParameterConverter
}
func (s *FakeStmt) Close() error {
// No connection added
if s.connection == nil {
panic("nil conn in FakeStmt.Close")
}
if s.connection.db == nil {
panic("in FakeStmt.Close, conn's db is nil (already closed)")
}
if !s.closed {
s.closed = true
}
if s.next != nil {
s.next.Close()
}
return nil
}
var errClosed = errors.New("fake_db_driver: statement has been closed")
func (smt *FakeStmt) Exec(args []driver.Value) (driver.Result, error) {
panic("Using ExecContext")
}
func (smt *FakeStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
if smt.closed {
return nil, errClosed
}
fResp := Catcher.FindResponse(smt.q, args)
// To emulate any exception during query which returns rows
if fResp.Exceptions != nil && fResp.Exceptions.HookExecBadConnection != nil && fResp.Exceptions.HookExecBadConnection() {
return nil, driver.ErrBadConn
}
if fResp.Callback != nil {
fResp.Callback(smt.q, args)
}
switch smt.command {
case "INSERT":
id := fResp.LastInsertId
if id == 0 {
id = rand.Int63()
}
res := NewFakeResult(id, 1)
return res, nil
case "UPDATE":
return driver.RowsAffected(fResp.RowsAffected), nil
}
return nil, fmt.Errorf("unimplemented statement Exec command type of %q", smt.command)
}
func (s *FakeStmt) Query(args []driver.Value) (driver.Rows, error) {
panic("Use QueryContext")
}
func (smt *FakeStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
if smt.closed {
return nil, errClosed
}
if len(args) > 0 {
// Replace all "?" to "%v" and replace them with the values after
for i := 0; i < len(args); i++ {
smt.q = strings.Replace(smt.q, "?", "%v", 1)
smt.q = fmt.Sprintf(smt.q, args[i].Value)
}
}
fResp := Catcher.FindResponse(smt.q, args)
if fResp.Exceptions != nil && fResp.Exceptions.HookQueryBadConnection != nil && fResp.Exceptions.HookQueryBadConnection() {
return nil, driver.ErrBadConn
}
resultRows := make([][]*row, 0, 1)
columnNames := make([]string, 0, 1)
columnTypes := make([][]string, 0, 1)
rows := []*row{}
// Check if we have such query in the map
colIndexes := make(map[string]int)
// Collecting column names from first record
if len(fResp.Response) > 0 {
for colName, _ := range fResp.Response[0] {
columnNames = append(columnNames, colName)
colIndexes[colName] = len(columnNames) - 1
}
}
// Extracting values from result according columns
for _, record := range fResp.Response {
oneRow := &row{cols: make([]interface{}, len(columnNames))}
for _, col := range columnNames {
oneRow.cols[colIndexes[col]] = []byte(record[col].(string))
}
rows = append(rows, oneRow)
}
resultRows = append(resultRows, rows)
cursor := &RowsCursor{
posRow: -1,
rows: resultRows,
cols: columnNames,
colType: columnTypes, // TODO: implement support of that
errPos: -1,
closed: false,
}
if fResp.Callback != nil {
fResp.Callback(smt.q, args)
}
return cursor, nil
}
// Returns number of args passed to query
func (s *FakeStmt) NumInput() int {
return s.placeholders
}
type FakeTx struct {
c *FakeConn
}
// hook to simulate broken connections
var HookBadCommit func() bool
func (tx *FakeTx) Commit() error {
tx.c.currTx = nil
if HookBadCommit != nil && HookBadCommit() {
return driver.ErrBadConn
}
return nil
}
// hook to simulate broken connections
var HookBadRollback func() bool
func (tx *FakeTx) Rollback() error {
tx.c.currTx = nil
if HookBadRollback != nil && HookBadRollback() {
return driver.ErrBadConn
}
return nil
}