1
0
mirror of https://github.com/zhashkevych/go-sqlxmock.git synced 2024-11-24 08:12:13 +02:00

implements named arguments support and adds delay expectation for context deadline simulation

This commit is contained in:
gedi 2017-02-06 14:38:57 +02:00
parent 55ecc5a333
commit d11f623794
4 changed files with 191 additions and 24 deletions

View File

@ -7,6 +7,7 @@ import (
"regexp"
"strings"
"sync"
"time"
)
// an expectation interface
@ -54,6 +55,7 @@ func (e *ExpectedClose) String() string {
// returned by *Sqlmock.ExpectBegin.
type ExpectedBegin struct {
commonExpectation
delay time.Duration
}
// WillReturnError allows to set an error for *sql.DB.Begin action
@ -71,6 +73,13 @@ func (e *ExpectedBegin) String() string {
return msg
}
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedBegin) WillDelayFor(duration time.Duration) *ExpectedBegin {
e.delay = duration
return e
}
// ExpectedCommit is used to manage *sql.Tx.Commit expectation
// returned by *Sqlmock.ExpectCommit.
type ExpectedCommit struct {
@ -118,7 +127,8 @@ func (e *ExpectedRollback) String() string {
// Returned by *Sqlmock.ExpectQuery.
type ExpectedQuery struct {
queryBasedExpectation
rows driver.Rows
rows driver.Rows
delay time.Duration
}
// WithArgs will match given expected args to actual database query arguments.
@ -142,6 +152,13 @@ func (e *ExpectedQuery) WillReturnRows(rows driver.Rows) *ExpectedQuery {
return e
}
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedQuery) WillDelayFor(duration time.Duration) *ExpectedQuery {
e.delay = duration
return e
}
// String returns string representation
func (e *ExpectedQuery) String() string {
msg := "ExpectedQuery => expecting Query or QueryRow which:"
@ -178,6 +195,7 @@ func (e *ExpectedQuery) String() string {
type ExpectedExec struct {
queryBasedExpectation
result driver.Result
delay time.Duration
}
// WithArgs will match given expected args to actual database exec operation arguments.
@ -194,6 +212,13 @@ func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec {
return e
}
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedExec) WillDelayFor(duration time.Duration) *ExpectedExec {
e.delay = duration
return e
}
// String returns string representation
func (e *ExpectedExec) String() string {
msg := "ExpectedExec => expecting Exec which:"
@ -244,6 +269,7 @@ type ExpectedPrepare struct {
sqlRegex *regexp.Regexp
statement driver.Stmt
closeErr error
delay time.Duration
}
// WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action.
@ -258,6 +284,13 @@ func (e *ExpectedPrepare) WillReturnCloseError(err error) *ExpectedPrepare {
return e
}
// WillDelayFor allows to specify duration for which it will delay
// result. May be used together with Context
func (e *ExpectedPrepare) WillDelayFor(duration time.Duration) *ExpectedPrepare {
e.delay = duration
return e
}
// ExpectQuery allows to expect Query() or QueryRow() on this prepared statement.
// this method is convenient in order to prevent duplicating sql query string matching.
func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery {
@ -300,7 +333,7 @@ type queryBasedExpectation struct {
args []driver.Value
}
func (e *queryBasedExpectation) attemptMatch(sql string, args []driver.Value) (err error) {
func (e *queryBasedExpectation) attemptMatch(sql string, args []namedValue) (err error) {
if !e.queryMatches(sql) {
return fmt.Errorf(`could not match sql: "%s" with expected regexp "%s"`, sql, e.sqlRegex.String())
}
@ -323,7 +356,7 @@ func (e *queryBasedExpectation) queryMatches(sql string) bool {
return e.sqlRegex.MatchString(sql)
}
func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
if nil == e.args {
return nil
}
@ -334,14 +367,26 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
// custom argument matcher
matcher, ok := e.args[k].(Argument)
if ok {
if !matcher.Match(v) {
// @TODO: does it make sense to pass value instead of named value?
if !matcher.Match(v.Value) {
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
}
continue
}
dval := e.args[k]
if named, isNamed := dval.(namedValue); isNamed {
dval = named.Value
if v.Name != named.Name {
return fmt.Errorf("named argument %d: name: \"%s\" does not match expected: \"%s\"", k, v.Name, named.Name)
}
if v.Ordinal != named.Ordinal {
return fmt.Errorf("named argument %d: ordinal position: \"%d\" does not match expected: \"%d\"", k, v.Ordinal, named.Ordinal)
}
}
// convert to driver converter
darg, err := driver.DefaultParameterConverter.ConvertValue(e.args[k])
darg, err := driver.DefaultParameterConverter.ConvertValue(dval)
if err != nil {
return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err)
}
@ -350,8 +395,8 @@ func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
}
if !reflect.DeepEqual(darg, args[k]) {
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, args[k], args[k])
if !reflect.DeepEqual(darg, v.Value) {
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value)
}
}
return nil

View File

@ -10,29 +10,38 @@ import (
func TestQueryExpectationArgComparison(t *testing.T) {
e := &queryBasedExpectation{}
against := []driver.Value{int64(5)}
against := []namedValue{{Value: int64(5), Ordinal: 1}}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err)
}
e.args = []driver.Value{5, "str"}
against = []driver.Value{int64(5)}
against = []namedValue{{Value: int64(5), Ordinal: 1}}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the size is not the same")
}
against = []driver.Value{int64(3), "str"}
against = []namedValue{
{Value: int64(3), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the first argument (int value) is different")
}
against = []driver.Value{int64(5), "st"}
against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "st", Ordinal: 2},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the second argument (string value) is different")
}
against = []driver.Value{int64(5), "str"}
against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, but it did not: %s", err)
}
@ -41,7 +50,10 @@ func TestQueryExpectationArgComparison(t *testing.T) {
tm, _ := time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
e.args = []driver.Value{5, tm}
against = []driver.Value{int64(5), tm}
against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: tm, Ordinal: 2},
}
if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, but it did not")
}
@ -52,29 +64,95 @@ func TestQueryExpectationArgComparison(t *testing.T) {
}
}
func TestQueryExpectationNamedArgComparison(t *testing.T) {
e := &queryBasedExpectation{}
against := []namedValue{{Value: int64(5), Name: "id"}}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err)
}
e.args = []driver.Value{
namedValue{Name: "id", Value: int64(5)},
namedValue{Name: "s", Value: "str"},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since the size is not the same")
}
against = []namedValue{
{Value: int64(5), Name: "id"},
{Value: "str", Name: "s"},
}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should have matched, but it did not: %v", err)
}
against = []namedValue{
{Value: int64(5), Name: "id"},
{Value: "str", Name: "username"},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments matched, but it should have not due to Name")
}
e.args = []driver.Value{
namedValue{Ordinal: 1, Value: int64(5)},
namedValue{Ordinal: 2, Value: "str"},
}
against = []namedValue{
{Value: int64(5), Ordinal: 0},
{Value: "str", Ordinal: 1},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments matched, but it should have not due to wrong Ordinal position")
}
against = []namedValue{
{Value: int64(5), Ordinal: 1},
{Value: "str", Ordinal: 2},
}
if err := e.argsMatches(against); err != nil {
t.Errorf("arguments should have matched, but it did not: %v", err)
}
}
func TestQueryExpectationArgComparisonBool(t *testing.T) {
var e *queryBasedExpectation
e = &queryBasedExpectation{args: []driver.Value{true}}
against := []driver.Value{true}
against := []namedValue{
{Value: true, Ordinal: 1},
}
if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, since arguments are the same")
}
e = &queryBasedExpectation{args: []driver.Value{false}}
against = []driver.Value{false}
against = []namedValue{
{Value: false, Ordinal: 1},
}
if err := e.argsMatches(against); err != nil {
t.Error("arguments should match, since argument are the same")
}
e = &queryBasedExpectation{args: []driver.Value{true}}
against = []driver.Value{false}
against = []namedValue{
{Value: false, Ordinal: 1},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since argument is different")
}
e = &queryBasedExpectation{args: []driver.Value{false}}
against = []driver.Value{true}
against = []namedValue{
{Value: true, Ordinal: 1},
}
if err := e.argsMatches(against); err == nil {
t.Error("arguments should not match, since argument is different")
}
@ -117,7 +195,7 @@ func TestBuildQuery(t *testing.T) {
name = 'John'
and
address = 'Jakarta'
`
mock.ExpectQuery(query)

View File

@ -15,6 +15,7 @@ import (
"database/sql/driver"
"fmt"
"regexp"
"time"
)
// Sqlmock interface serves to create expectations
@ -184,6 +185,7 @@ func (c *sqlmock) Begin() (driver.Tx, error) {
expected.triggered = true
expected.Unlock()
defer time.Sleep(expected.delay)
return c, expected.err
}
@ -194,7 +196,18 @@ func (c *sqlmock) ExpectBegin() *ExpectedBegin {
}
// Exec meets http://golang.org/pkg/database/sql/driver/#Execer
func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, err error) {
func (c *sqlmock) Exec(query string, args []driver.Value) (driver.Result, error) {
namedArgs := make([]namedValue, len(args))
for i, v := range args {
namedArgs[i] = namedValue{
Ordinal: i + 1,
Value: v,
}
}
return c.exec(nil, query, namedArgs)
}
func (c *sqlmock) exec(ctx interface{}, query string, args []namedValue) (res driver.Result, err error) {
query = stripQuery(query)
var expected *ExpectedExec
var fulfilled int
@ -230,17 +243,19 @@ func (c *sqlmock) Exec(query string, args []driver.Value) (res driver.Result, er
return nil, fmt.Errorf(msg, query, args)
}
defer expected.Unlock()
if !expected.queryMatches(query) {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', does not match regex '%s'", query, expected.sqlRegex.String())
}
if err := expected.argsMatches(args); err != nil {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
}
expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
if expected.err != nil {
return nil, expected.err // mocked to return error
@ -292,12 +307,14 @@ func (c *sqlmock) Prepare(query string) (driver.Stmt, error) {
}
return nil, fmt.Errorf(msg, query)
}
defer expected.Unlock()
if !expected.sqlRegex.MatchString(query) {
expected.Unlock()
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
}
expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
return &statement{c, query, expected.closeErr}, expected.err
}
@ -308,8 +325,27 @@ func (c *sqlmock) ExpectPrepare(sqlRegexStr string) *ExpectedPrepare {
return e
}
type namedValue struct {
Name string
Ordinal int
Value driver.Value
}
// Query meets http://golang.org/pkg/database/sql/driver/#Queryer
func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err error) {
namedArgs := make([]namedValue, len(args))
for i, v := range args {
namedArgs[i] = namedValue{
Ordinal: i + 1,
Value: v,
}
}
return c.query(nil, query, namedArgs)
}
// in order to prevent dependencies, we use Context as a plain interface
// since it is only related to internal implementation
func (c *sqlmock) query(ctx interface{}, query string, args []namedValue) (rw driver.Rows, err error) {
query = stripQuery(query)
var expected *ExpectedQuery
var fulfilled int
@ -346,18 +382,21 @@ func (c *sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err
return nil, fmt.Errorf(msg, query, args)
}
defer expected.Unlock()
if !expected.queryMatches(query) {
expected.Unlock()
return nil, fmt.Errorf("query '%s', does not match regex [%s]", query, expected.sqlRegex.String())
}
if err := expected.argsMatches(args); err != nil {
expected.Unlock()
return nil, fmt.Errorf("exec query '%s', arguments do not match: %s", query, err)
}
expected.triggered = true
defer time.Sleep(expected.delay)
defer expected.Unlock()
if expected.err != nil {
return nil, expected.err // mocked to return error
}

5
sqlmock_go18.go Normal file
View File

@ -0,0 +1,5 @@
// +build go1.8
package sqlmock
// @TODO context based extensions