diff --git a/expectations_test.go b/expectations_test.go index cbb13a9..8bc37cd 100644 --- a/expectations_test.go +++ b/expectations_test.go @@ -2,6 +2,7 @@ package sqlmock import ( "database/sql/driver" + "fmt" "regexp" "testing" "time" @@ -71,3 +72,13 @@ func TestQueryExpectationSqlMatch(t *testing.T) { t.Errorf("Sql must have matched the query") } } + +func ExampleExpectExec() { + db, mock, _ := New() + result := NewErrorResult(fmt.Errorf("some error")) + mock.ExpectExec("^INSERT (.+)").WillReturnResult(result) + res, _ := db.Exec("INSERT something") + _, err := res.LastInsertId() + fmt.Println(err) + // Output: some error +} diff --git a/result_test.go b/result_test.go index 3f5de21..b735c87 100644 --- a/result_test.go +++ b/result_test.go @@ -9,14 +9,21 @@ import ( var mock = &Sqlmock{} func ExampleNewErrorResult() { + db, mock, _ := New() result := NewErrorResult(fmt.Errorf("some error")) mock.ExpectExec("^INSERT (.+)").WillReturnResult(result) + res, _ := db.Exec("INSERT something") + _, err := res.LastInsertId() + fmt.Println(err) + // Output: some error } func ExampleNewResult() { var lastInsertID, affected int64 result := NewResult(lastInsertID, affected) mock.ExpectExec("^INSERT (.+)").WillReturnResult(result) + fmt.Println(mock.ExpectationsWereMet()) + // Output: there is a remaining expectation *sqlmock.ExpectedExec which was not matched yet } func TestShouldReturnValidSqlDriverResult(t *testing.T) { diff --git a/rows.go b/rows.go index 3e58a89..62a4cf0 100644 --- a/rows.go +++ b/rows.go @@ -17,19 +17,35 @@ type Rows interface { // return the same instance to perform subsequent actions. // Note that the number of values must match the number // of columns - AddRow(...driver.Value) Rows + AddRow(columns ...driver.Value) Rows // FromCSVString build rows from csv string. // return the same instance to perform subsequent actions. // Note that the number of values must match the number // of columns FromCSVString(s string) Rows + + // RowError allows to set an error + // which will be returned when a given + // row number is read + RowError(row int, err error) Rows + + // CloseError allows to set an error + // which will be returned by rows.Close + // function. + // + // The close error will be triggered only in cases + // when rows.Next() EOF was not yet reached, that is + // a default sql library behavior + CloseError(err error) Rows } type rows struct { - cols []string - rows [][]driver.Value - pos int + cols []string + rows [][]driver.Value + pos int + scanErr map[int]error + closeErr error } func (r *rows) Columns() []string { @@ -37,11 +53,7 @@ func (r *rows) Columns() []string { } func (r *rows) Close() error { - return nil -} - -func (r *rows) Err() error { - return nil + return r.closeErr } // advances to next row @@ -55,14 +67,24 @@ func (r *rows) Next(dest []driver.Value) error { dest[i] = col } - return nil + return r.scanErr[r.pos-1] } // NewRows allows Rows to be created from a // sql driver.Value slice or from the CSV string and // to be used as sql driver.Rows func NewRows(columns []string) Rows { - return &rows{cols: columns} + return &rows{cols: columns, scanErr: make(map[int]error)} +} + +func (r *rows) CloseError(err error) Rows { + r.closeErr = err + return r +} + +func (r *rows) RowError(row int, err error) Rows { + r.scanErr[row] = err + return r } func (r *rows) AddRow(values ...driver.Value) Rows { diff --git a/rows_test.go b/rows_test.go new file mode 100644 index 0000000..9347340 --- /dev/null +++ b/rows_test.go @@ -0,0 +1,209 @@ +package sqlmock + +import ( + "database/sql" + "fmt" + "testing" +) + +func ExampleRows() { + db, mock, err := New() + if err != nil { + fmt.Println("failed to open sqlmock database:", err) + } + defer db.Close() + + rows := NewRows([]string{"id", "title"}). + AddRow(1, "one"). + AddRow(2, "two") + + 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 +} + +func ExampleRows_rowError() { + db, mock, err := New() + if err != nil { + fmt.Println("failed to open sqlmock database:", err) + } + defer db.Close() + + rows := NewRows([]string{"id", "title"}). + AddRow(0, "one"). + AddRow(1, "two"). + RowError(1, fmt.Errorf("row error")) + 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: 0 and title: one + // got rows error: row error +} + +func ExampleRows_closeError() { + db, mock, err := New() + if err != nil { + fmt.Println("failed to open sqlmock database:", err) + } + defer db.Close() + + rows := NewRows([]string{"id", "title"}).CloseError(fmt.Errorf("close error")) + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + rs, _ := db.Query("SELECT") + + // Note: that close will return error only before rows EOF + // that is a default sql package behavior. If you run rs.Next() + // it will handle the error internally and return nil bellow + if err := rs.Close(); err != nil { + fmt.Println("got error:", err) + } + + // Output: got error: close error +} + +func TestAllowsToSetRowsErrors(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() + + rows := NewRows([]string{"id", "title"}). + AddRow(0, "one"). + AddRow(1, "two"). + RowError(1, fmt.Errorf("error")) + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + rs, err := db.Query("SELECT") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer rs.Close() + + if !rs.Next() { + t.Fatal("expected the first row to be available") + } + if rs.Err() != nil { + t.Fatalf("unexpected error: %s", rs.Err()) + } + + if rs.Next() { + t.Fatal("was not expecting the second row, since there should be an error") + } + if rs.Err() == nil { + t.Fatal("expected an error, but got none") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestRowsCloseError(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() + + rows := NewRows([]string{"id"}).CloseError(fmt.Errorf("close error")) + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + rs, err := db.Query("SELECT") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if err := rs.Close(); err == nil { + t.Fatal("expected a close error") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestQuerySingleRow(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() + + rows := NewRows([]string{"id"}). + AddRow(1). + AddRow(2) + mock.ExpectQuery("SELECT").WillReturnRows(rows) + + var id int + if err := db.QueryRow("SELECT").Scan(&id); err != nil { + t.Fatalf("unexpected error: %s", err) + } + + mock.ExpectQuery("SELECT").WillReturnRows(NewRows([]string{"id"})) + if err := db.QueryRow("SELECT").Scan(&id); err != sql.ErrNoRows { + t.Fatal("expected sql no rows error") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +// @TODO: the only way to mock columns error would be +// to return nil instead of rows, but rows.Close will +// panic due to a bug in go sql package +func TODO_TestRowsColumnsError(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() + + mock.ExpectQuery("SELECT").WillReturnRows(nil) + + rs, err := db.Query("SELECT") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + _, err = rs.Columns() + if err == nil { + t.Fatal("expected an error for columns") + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} diff --git a/sqlmock.go b/sqlmock.go index 4a44673..565453a 100644 --- a/sqlmock.go +++ b/sqlmock.go @@ -201,6 +201,7 @@ func (c *Sqlmock) Query(query string, args []driver.Value) (rw driver.Rows, err return nil, t.err // mocked to return error } + // remove when rows_test.go:186 is available, won't cause a BC if t.rows == nil { return nil, fmt.Errorf("query '%s' with args %+v, must return a database/sql/driver.rows, but it was not set for expectation %T as %+v", query, args, t, t) }