diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..777c734 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug Report +about: Let us know about an unexpected error, or an incorrect behavior. +labels: "type/bug" +--- + + +### Operating system and Go Version + +### Issue + +### Reproduction steps + +#### Expected Result + +#### Actual Result + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6d35c11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Curious About Something? + url: https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock + about: If you have a curious about something that isn't totally clear, please checkout the documentation first before creating an issue. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4a4a7f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement to this project +labels: "type/enhancement" +--- + + + +### Proposal + + +### Use-cases + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..cf68c29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,11 @@ +--- +name: Question +about: Ask a question +labels: "type/question" +--- + +### Question + + diff --git a/.travis.yml b/.travis.yml index 1312df7..5594029 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,9 @@ go: - 1.12.x - 1.13.x - 1.14.x + - 1.15.x + - 1.16.x + - 1.17.x script: - go vet diff --git a/README.md b/README.md index d4151f4..34da8f0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ import ( ) func recordStats(db *sql.DB, userID, productID int64) (err error) { - tx, err = db.Begin() + tx, err := db.Begin() if err != nil { return } @@ -193,7 +193,7 @@ func (a AnyTime) Match(v driver.Value) bool { func TestAnyTimeArgument(t *testing.T) { t.Parallel() - db, mock, err := New() + db, mock, err := sqlmock.New() if err != nil { t.Errorf("an error '%s' was not expected when opening a stub database connection", err) } @@ -201,7 +201,7 @@ func TestAnyTimeArgument(t *testing.T) { mock.ExpectExec("INSERT INTO users"). WithArgs("john", AnyTime{}). - WillReturnResult(NewResult(1, 1)) + WillReturnResult(sqlmock.NewResult(1, 1)) _, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now()) if err != nil { diff --git a/expectations.go b/expectations.go index 5c82c7b..8a6cd44 100644 --- a/expectations.go +++ b/expectations.go @@ -134,11 +134,27 @@ type ExpectedQuery struct { // WithArgs will match given expected args to actual database query arguments. // if at least one argument does not match, it will return an error. For specific // arguments an sqlmock.Argument interface can be used to match an argument. +// Must not be used together with WithoutArgs() func (e *ExpectedQuery) WithArgs(args ...driver.Value) *ExpectedQuery { + if e.noArgs { + panic("WithArgs() and WithoutArgs() must not be used together") + } e.args = args return e } +// WithoutArgs will ensure that no arguments are passed for this query. +// if at least one argument is passed, it will return an error. This allows +// for stricter validation of the query arguments. +// Must no be used together with WithArgs() +func (e *ExpectedQuery) WithoutArgs() *ExpectedQuery { + if len(e.args) > 0 { + panic("WithoutArgs() and WithArgs() must not be used together") + } + e.noArgs = true + return e +} + // RowsWillBeClosed expects this query rows to be closed. func (e *ExpectedQuery) RowsWillBeClosed() *ExpectedQuery { e.rowsMustBeClosed = true @@ -195,11 +211,27 @@ type ExpectedExec struct { // WithArgs will match given expected args to actual database exec operation arguments. // if at least one argument does not match, it will return an error. For specific // arguments an sqlmock.Argument interface can be used to match an argument. +// Must not be used together with WithoutArgs() func (e *ExpectedExec) WithArgs(args ...driver.Value) *ExpectedExec { + if len(e.args) > 0 { + panic("WithArgs() and WithoutArgs() must not be used together") + } e.args = args return e } +// WithoutArgs will ensure that no args are passed for this expected database exec action. +// if at least one argument is passed, it will return an error. This allows for stricter +// validation of the query arguments. +// Must not be used together with WithArgs() +func (e *ExpectedExec) WithoutArgs() *ExpectedExec { + if len(e.args) > 0 { + panic("WithoutArgs() and WithArgs() must not be used together") + } + e.noArgs = true + return e +} + // WillReturnError allows to set an error for expected database exec action func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec { e.err = err @@ -230,12 +262,13 @@ func (e *ExpectedExec) String() string { } if e.result != nil { - res, _ := e.result.(*result) - msg += "\n - should return Result having:" - msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID) - msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected) - if res.err != nil { - msg += fmt.Sprintf("\n Error: %s", res.err) + if res, ok := e.result.(*result); ok { + msg += "\n - should return Result having:" + msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID) + msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected) + if res.err != nil { + msg += fmt.Sprintf("\n Error: %s", res.err) + } } } @@ -337,6 +370,7 @@ type queryBasedExpectation struct { expectSQL string converter driver.ValueConverter args []driver.Value + noArgs bool // ensure no args are passed } // ExpectedPing is used to manage *sql.DB.Ping expectations. diff --git a/expectations_before_go18.go b/expectations_before_go18.go index f6e7b4e..67c08dc 100644 --- a/expectations_before_go18.go +++ b/expectations_before_go18.go @@ -1,3 +1,4 @@ +//go:build !go1.8 // +build !go1.8 package sqlmock @@ -17,6 +18,9 @@ func (e *ExpectedQuery) WillReturnRows(rows *Rows) *ExpectedQuery { func (e *queryBasedExpectation) argsMatches(args []namedValue) error { if nil == e.args { + if e.noArgs && len(args) > 0 { + return fmt.Errorf("expected 0, but got %d arguments", len(args)) + } return nil } if len(args) != len(e.args) { diff --git a/expectations_before_go18_test.go b/expectations_before_go18_test.go index 897ebff..4234cd6 100644 --- a/expectations_before_go18_test.go +++ b/expectations_before_go18_test.go @@ -1,3 +1,4 @@ +//go:build !go1.8 // +build !go1.8 package sqlmock @@ -9,10 +10,15 @@ import ( ) func TestQueryExpectationArgComparison(t *testing.T) { - e := &queryBasedExpectation{converter: driver.DefaultParameterConverter} + e := &queryBasedExpectation{converter: driver.DefaultParameterConverter, noArgs: true} against := []namedValue{{Value: int64(5), Ordinal: 1}} + if err := e.argsMatches(against); err == nil { + t.Error("arguments should not match, since argument was passed, but noArgs was set") + } + + e.noArgs = false if err := e.argsMatches(against); err != nil { - t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err) + t.Error("arguments should match, since argument was passed, but no expected args or noArgs was set") } e.args = []driver.Value{5, "str"} diff --git a/expectations_go18.go b/expectations_go18.go index 6b85ce1..07227ed 100644 --- a/expectations_go18.go +++ b/expectations_go18.go @@ -1,3 +1,4 @@ +//go:build go1.8 // +build go1.8 package sqlmock @@ -30,6 +31,9 @@ func (e *ExpectedQuery) WillReturnRows(rows ...*Rows) *ExpectedQuery { func (e *queryBasedExpectation) argsMatches(args []driver.NamedValue) error { if nil == e.args { + if e.noArgs && len(args) > 0 { + return fmt.Errorf("expected 0, but got %d arguments", len(args)) + } return nil } if len(args) != len(e.args) { diff --git a/expectations_go18_test.go b/expectations_go18_test.go index 1974721..cd633b7 100644 --- a/expectations_go18_test.go +++ b/expectations_go18_test.go @@ -1,3 +1,4 @@ +//go:build go1.8 // +build go1.8 package sqlmock @@ -10,10 +11,15 @@ import ( ) func TestQueryExpectationArgComparison(t *testing.T) { - e := &queryBasedExpectation{converter: driver.DefaultParameterConverter} + e := &queryBasedExpectation{converter: driver.DefaultParameterConverter, noArgs: true} against := []driver.NamedValue{{Value: int64(5), Ordinal: 1}} + if err := e.argsMatches(against); err == nil { + t.Error("arguments should not match, since argument was passed, but noArgs was set") + } + + e.noArgs = false if err := e.argsMatches(against); err != nil { - t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err) + t.Error("arguments should match, since argument was passed, but no expected args or noArgs was set") } e.args = []driver.Value{5, "str"} @@ -102,10 +108,15 @@ func TestQueryExpectationArgComparisonBool(t *testing.T) { } func TestQueryExpectationNamedArgComparison(t *testing.T) { - e := &queryBasedExpectation{converter: driver.DefaultParameterConverter} + e := &queryBasedExpectation{converter: driver.DefaultParameterConverter, noArgs: true} against := []driver.NamedValue{{Value: int64(5), Name: "id"}} + if err := e.argsMatches(against); err == nil { + t.Error("arguments should not match, since argument was passed, but noArgs was set") + } + + e.noArgs = false if err := e.argsMatches(against); err != nil { - t.Errorf("arguments should match, since the no expectation was set, but got err: %s", err) + t.Error("arguments should match, since argument was passed, but no expected args or noArgs was set") } e.args = []driver.Value{ diff --git a/expectations_test.go b/expectations_test.go index afda582..cf0251a 100644 --- a/expectations_test.go +++ b/expectations_test.go @@ -101,3 +101,25 @@ func TestCustomValueConverterQueryScan(t *testing.T) { t.Error(err) } } + +func TestQueryWithNoArgsAndWithArgsPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + return + } + t.Error("Expected panic for using WithArgs and ExpectNoArgs together") + }() + mock := &sqlmock{} + mock.ExpectQuery("SELECT (.+) FROM user").WithArgs("John").WithoutArgs() +} + +func TestExecWithNoArgsAndWithArgsPanic(t *testing.T) { + defer func() { + if r := recover(); r != nil { + return + } + t.Error("Expected panic for using WithArgs and ExpectNoArgs together") + }() + mock := &sqlmock{} + mock.ExpectExec("^INSERT INTO user").WithArgs("John").WithoutArgs() +} diff --git a/go.mod b/go.mod index eaf8a5a..6f58b70 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,5 @@ module github.com/DATA-DOG/go-sqlmock + +go 1.15 + +require github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a21f637 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= diff --git a/rows.go b/rows.go index ccc5f0c..01ea811 100644 --- a/rows.go +++ b/rows.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql/driver" "encoding/csv" + "errors" "fmt" "io" "strings" @@ -14,7 +15,7 @@ const invalidate = "☠☠☠ MEMORY OVERWRITTEN ☠☠☠ " // CSVColumnParser is a function which converts trimmed csv // column string to a []byte representation. Currently // transforms NULL to nil -var CSVColumnParser = func(s string) []byte { +var CSVColumnParser = func(s string) interface{} { switch { case strings.ToLower(s) == "null": return nil @@ -165,7 +166,7 @@ func (r *Rows) RowError(row int, err error) *Rows { // of columns func (r *Rows) AddRow(values ...driver.Value) *Rows { if len(values) != len(r.cols) { - panic("Expected number of values to match number of columns") + panic(fmt.Sprintf("Expected number of values to match number of columns: expected %d, actual %d", len(values), len(r.cols))) } row := make([]driver.Value, len(r.cols)) @@ -188,6 +189,16 @@ func (r *Rows) AddRow(values ...driver.Value) *Rows { return r } +// AddRows adds multiple rows composed from database driver.Value slice and +// returns the same instance to perform subsequent actions. +func (r *Rows) AddRows(values ...[]driver.Value) *Rows { + for _, value := range values { + r.AddRow(value...) + } + + return r +} + // FromCSVString build rows from csv string. // return the same instance to perform subsequent actions. // Note that the number of values must match the number @@ -198,8 +209,11 @@ func (r *Rows) FromCSVString(s string) *Rows { for { res, err := csvReader.Read() - if err != nil || res == nil { - break + if err != nil { + if errors.Is(err, io.EOF) { + break + } + panic(fmt.Sprintf("Parsing CSV string failed: %s", err.Error())) } row := make([]driver.Value, len(r.cols)) diff --git a/rows_test.go b/rows_test.go index 15cdbee..80f1476 100644 --- a/rows_test.go +++ b/rows_test.go @@ -3,6 +3,7 @@ package sqlmock import ( "bytes" "database/sql" + "database/sql/driver" "fmt" "testing" ) @@ -431,7 +432,7 @@ func TestRowsScanError(t *testing.T) { func TestCSVRowParser(t *testing.T) { t.Parallel() - rs := NewRows([]string{"col1", "col2"}).FromCSVString("a,NULL") + rs := NewRows([]string{"col1", "col2", "col3"}).FromCSVString("a,NULL,NULL") db, mock, err := New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) @@ -447,9 +448,10 @@ func TestCSVRowParser(t *testing.T) { defer rw.Close() var col1 string var col2 []byte + var col3 *string rw.Next() - if err = rw.Scan(&col1, &col2); err != nil { + if err = rw.Scan(&col1, &col2, &col3); err != nil { t.Fatalf("unexpected error: %s", err) } if col1 != "a" { @@ -458,6 +460,18 @@ func TestCSVRowParser(t *testing.T) { if col2 != nil { t.Fatalf("expected col2 to be nil, but got [%T]:%+v", col2, col2) } + if col3 != nil { + t.Fatalf("expected col3 to be nil, but got [%T]:%+v", col3, col3) + } +} + +func TestCSVParserInvalidInput(t *testing.T) { + defer func() { + recover() + }() + _ = NewRows([]string{"col1", "col2"}).FromCSVString("a,\"NULL\"\"") + // shouldn't reach here + t.Error("expected panic from parsing invalid CSV") } func TestWrongNumberOfValues(t *testing.T) { @@ -670,3 +684,110 @@ func queryRowBytesNotInvalidatedByClose(t *testing.T, rows *Rows, scan func(*sql t.Fatal(err) } } + +func TestAddRows(t *testing.T) { + t.Parallel() + db, mock, err := New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + values := [][]driver.Value{ + { + 1, "John", + }, + { + 2, "Jane", + }, + { + 3, "Peter", + }, + { + 4, "Emily", + }, + } + + rows := NewRows([]string{"id", "name"}).AddRows(values...) + mock.ExpectQuery("SELECT").WillReturnRows(rows).RowsWillBeClosed() + + rs, _ := db.Query("SELECT") + defer rs.Close() + + for rs.Next() { + var id int + var name string + rs.Scan(&id, &name) + fmt.Println("scanned id:", id, "and name:", name) + } + + if rs.Err() != nil { + fmt.Println("got rows error:", rs.Err()) + } + // Output: scanned id: 1 and title: John + // scanned id: 2 and title: Jane + // scanned id: 3 and title: Peter + // scanned id: 4 and title: Emily +} + +func TestAddRowExpectPanic(t *testing.T) { + t.Parallel() + + const expectedPanic = "Expected number of values to match number of columns: expected 1, actual 2" + values := []driver.Value{ + "John", + "Jane", + } + + defer func() { + if r := recover(); r != nil { + if r != expectedPanic { + t.Fatalf("panic message did not match expected: expected '%s', actual '%s'", r, expectedPanic) + } + + return + } + t.Fatalf("expected panic: %s", expectedPanic) + }() + + rows := NewRows([]string{"id", "name"}) + // Note missing spread "..." + rows.AddRow(values) +} + +func ExampleRows_AddRows() { + db, mock, err := New() + if err != nil { + fmt.Println("failed to open sqlmock database:", err) + } + defer db.Close() + + values := [][]driver.Value{ + { + 1, "one", + }, + { + 2, "two", + }, + } + + rows := NewRows([]string{"id", "title"}).AddRows(values...) + + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + rs, _ := db.Query("SELECT") + defer rs.Close() + + for rs.Next() { + var id int + var title string + rs.Scan(&id, &title) + fmt.Println("scanned id:", id, "and title:", title) + } + + if rs.Err() != nil { + fmt.Println("got rows error:", rs.Err()) + } + // Output: scanned id: 1 and title: one + // scanned id: 2 and title: two +} diff --git a/sqlmock_go18_test.go b/sqlmock_go18_test.go index 223e076..6267f38 100644 --- a/sqlmock_go18_test.go +++ b/sqlmock_go18_test.go @@ -1,3 +1,4 @@ +//go:build go1.8 // +build go1.8 package sqlmock @@ -435,8 +436,7 @@ func TestContextExecErrorDelay(t *testing.T) { defer db.Close() // test that return of error is delayed - var delay time.Duration - delay = 100 * time.Millisecond + var delay time.Duration = 100 * time.Millisecond mock.ExpectExec("^INSERT INTO articles"). WillReturnError(errors.New("slow fail")). WillDelayFor(delay) diff --git a/sqlmock_test.go b/sqlmock_test.go index ee6b516..2129a16 100644 --- a/sqlmock_test.go +++ b/sqlmock_test.go @@ -749,6 +749,16 @@ func TestRunExecsWithExpectedErrorMeetsExpectations(t *testing.T) { } } +func TestRunExecsWithNoArgsExpectedMeetsExpectations(t *testing.T) { + db, dbmock, _ := New() + dbmock.ExpectExec("THE FIRST EXEC").WithoutArgs().WillReturnResult(NewResult(0, 0)) + + _, err := db.Exec("THE FIRST EXEC", "foobar") + if err == nil { + t.Fatalf("expected error, but there wasn't any") + } +} + func TestRunQueryWithExpectedErrorMeetsExpectations(t *testing.T) { db, dbmock, _ := New() dbmock.ExpectQuery("THE FIRST QUERY").WillReturnError(fmt.Errorf("big bad bug")) @@ -1102,8 +1112,7 @@ func TestExecExpectationErrorDelay(t *testing.T) { defer db.Close() // test that return of error is delayed - var delay time.Duration - delay = 100 * time.Millisecond + var delay time.Duration = 100 * time.Millisecond mock.ExpectExec("^INSERT INTO articles"). WillReturnError(errors.New("slow fail")). WillDelayFor(delay) @@ -1340,3 +1349,19 @@ func Test_sqlmock_Query(t *testing.T) { return } } + +func Test_sqlmock_QueryExpectWithoutArgs(t *testing.T) { + db, mock, err := New() + if err != nil { + t.Errorf("an error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + expectedRows := mock.NewRows([]string{"id", "name", "email"}).AddRow(1, "test", "test@example.com") + mock.ExpectQuery("SELECT (.+) FROM users WHERE (.+)").WillReturnRows(expectedRows).WithoutArgs() + query := "SELECT name, email FROM users WHERE name = ?" + _, err = mock.(*sqlmock).Query(query, []driver.Value{"test"}) + if err == nil { + t.Errorf("error expected") + return + } +}