Skip to content

Commit

Permalink
Merge pull request #68 from DATA-DOG/go1.8
Browse files Browse the repository at this point in the history
Support for go 1.8 SQL features
  • Loading branch information
l3pp4rd authored Feb 21, 2017
2 parents 55ecc5a + 372a183 commit b983233
Show file tree
Hide file tree
Showing 14 changed files with 1,030 additions and 125 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go:
- 1.5
- 1.6
- 1.7
- 1.8
- tip

script: go test -race -coverprofile=coverage.txt -covermode=atomic
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses)

Copyright (c) 2013-2016, DATA-DOG team
Copyright (c) 2013-2017, DATA-DOG team
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,16 @@ maintain correct **TDD** workflow.

- this library is now complete and stable. (you may not find new changes for this reason)
- supports concurrency and multiple connections.
- supports **go1.8** Context related feature mocking and Named sql parameters.
- does not require any modifications to your source code.
- the driver allows to mock any sql driver method behavior.
- has strict by default expectation order matching.
- has no vendor dependencies.
- has no third party dependencies.

## Install

go get gopkg.in/DATA-DOG/go-sqlmock.v1

If you need an old version, checkout **go-sqlmock** at gopkg.in:

go get gopkg.in/DATA-DOG/go-sqlmock.v0

## Documentation and Examples

Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference.
Expand Down Expand Up @@ -187,8 +184,11 @@ It only asserts that argument is of `time.Time` type.

go test -race

## Changes
## Change Log

- **2017-02-09** - implemented support for **go1.8** features. **Rows** interface was changed to struct
but contains all methods as before and should maintain backwards compatibility. **ExpectedQuery.WillReturnRows** may now
accept multiple row sets.
- **2016-11-02** - `db.Prepare()` was not validating expected prepare SQL
query. It should still be validated even if Exec or Query is not
executed on that prepared statement.
Expand Down
80 changes: 33 additions & 47 deletions expectations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package sqlmock
import (
"database/sql/driver"
"fmt"
"reflect"
"regexp"
"strings"
"sync"
"time"
)

// an expectation interface
Expand Down Expand Up @@ -54,6 +54,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
Expand All @@ -71,6 +72,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 {
Expand Down Expand Up @@ -118,7 +126,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.
Expand All @@ -135,10 +144,10 @@ func (e *ExpectedQuery) WillReturnError(err error) *ExpectedQuery {
return e
}

// WillReturnRows specifies the set of resulting rows that will be returned
// by the triggered query
func (e *ExpectedQuery) WillReturnRows(rows driver.Rows) *ExpectedQuery {
e.rows = rows
// 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
}

Expand All @@ -158,12 +167,7 @@ func (e *ExpectedQuery) String() string {
}

if e.rows != nil {
msg += "\n - should return rows:\n"
rs, _ := e.rows.(*rows)
for i, row := range rs.rows {
msg += fmt.Sprintf(" %d - %+v\n", i, row)
}
msg = strings.TrimSpace(msg)
msg += fmt.Sprintf("\n - %s", e.rows)
}

if e.err != nil {
Expand All @@ -178,6 +182,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.
Expand All @@ -194,6 +199,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:"
Expand Down Expand Up @@ -244,6 +256,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.
Expand All @@ -258,6 +271,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 {
Expand Down Expand Up @@ -300,7 +320,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())
}
Expand All @@ -322,37 +342,3 @@ func (e *queryBasedExpectation) attemptMatch(sql string, args []driver.Value) (e
func (e *queryBasedExpectation) queryMatches(sql string) bool {
return e.sqlRegex.MatchString(sql)
}

func (e *queryBasedExpectation) argsMatches(args []driver.Value) error {
if nil == e.args {
return nil
}
if len(args) != len(e.args) {
return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args))
}
for k, v := range args {
// custom argument matcher
matcher, ok := e.args[k].(Argument)
if ok {
if !matcher.Match(v) {
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
}
continue
}

// convert to driver converter
darg, err := driver.DefaultParameterConverter.ConvertValue(e.args[k])
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)
}

if !driver.IsValue(darg) {
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])
}
}
return nil
}
52 changes: 52 additions & 0 deletions expectations_before_go18.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// +build !go1.8

package sqlmock

import (
"database/sql/driver"
"fmt"
"reflect"
)

// WillReturnRows specifies the set of resulting rows that will be returned
// by the triggered query
func (e *ExpectedQuery) WillReturnRows(rows *Rows) *ExpectedQuery {
e.rows = &rowSets{sets: []*Rows{rows}}
return e
}

func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
if nil == e.args {
return nil
}
if len(args) != len(e.args) {
return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args))
}
for k, v := range args {
// custom argument matcher
matcher, ok := e.args[k].(Argument)
if ok {
// @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]
// convert to driver converter
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)
}

if !driver.IsValue(darg) {
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
}

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
}
66 changes: 66 additions & 0 deletions expectations_go18.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// +build go1.8

package sqlmock

import (
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
)

// WillReturnRows specifies the set of resulting rows that will be returned
// by the triggered query
func (e *ExpectedQuery) WillReturnRows(rows ...*Rows) *ExpectedQuery {
sets := make([]*Rows, len(rows))
for i, r := range rows {
sets[i] = r
}
e.rows = &rowSets{sets: sets}
return e
}

func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
if nil == e.args {
return nil
}
if len(args) != len(e.args) {
return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args))
}
// @TODO should we assert either all args are named or ordinal?
for k, v := range args {
// custom argument matcher
matcher, ok := e.args[k].(Argument)
if ok {
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.(sql.NamedArg); 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)
}
} else if k+1 != v.Ordinal {
return fmt.Errorf("argument %d: ordinal position: %d does not match expected: %d", k, k+1, v.Ordinal)
}

// convert to driver converter
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)
}

if !driver.IsValue(darg) {
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
}

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
}
64 changes: 64 additions & 0 deletions expectations_go18_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// +build go1.8

package sqlmock

import (
"database/sql"
"database/sql/driver"
"testing"
)

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{
sql.Named("id", 5),
sql.Named("s", "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{int64(5), "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)
}
}
Loading

0 comments on commit b983233

Please sign in to comment.