From fb65f6708c82f9b87e8ce36fa5c049197665293f Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 16 Nov 2017 18:40:49 +0800 Subject: [PATCH 01/39] Groundwork for TestHelper --- gorm_expect.go | 108 ++++++++++++++++++++++++++++++++++++++++++++ gorm_expect_test.go | 26 +++++++++++ 2 files changed, 134 insertions(+) create mode 100644 gorm_expect.go create mode 100644 gorm_expect_test.go diff --git a/gorm_expect.go b/gorm_expect.go new file mode 100644 index 00000000..4c29231f --- /dev/null +++ b/gorm_expect.go @@ -0,0 +1,108 @@ +package gorm + +import ( + "database/sql" + "fmt" + + "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +// TestHelper is the exported struct used for setting expectations +type TestHelper struct { + gormDb *DB + mockDb *sql.DB + asserter Asserter +} + +func (h *TestHelper) ExpectFirst(model interface{}) Query { + fmt.Printf("Expecting query: %s", "some query\n") + return h.asserter.Query("some sql") +} + +func (h *TestHelper) ExpectFind(model interface{}) { + fmt.Println("Expecting query: %s", "some query involving Find") +} + +type Query interface { + Return(model interface{}) Query +} + +type SqlmockQuery struct { + query *sqlmock.ExpectedQuery +} + +func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { + rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) + rows = rows.AddRow("someval1", "someval2", "someval3") + + return rows +} + +func (q *SqlmockQuery) Return(out interface{}) Query { + rows := q.getRowsForOutType(out) + q.query = q.query.WillReturnRows(rows) + + return q +} + +type Exec interface { + Return(args ...interface{}) +} + +type Adapter interface { + Open() (error, *sql.DB, *DB, Asserter) + Close() error +} + +type Asserter interface { + Query(query string) Query + // Exec(stmt string) Exec +} + +type SqlmockAdapter struct { + mockDb *sql.DB + mock *sqlmock.Sqlmock +} + +// Open returns the raw sql.DB and a gorm DB instance +func (adapter *SqlmockAdapter) Open() (error, *sql.DB, *DB, Asserter) { + mockDb, mock, err := sqlmock.NewWithDSN("mock_gorm_dsn") + + if err != nil { + return err, nil, nil, nil + } + + gormDb, err := Open("sqlmock", "mock_gorm_dsn") + + if err != nil { + return err, nil, nil, nil + } + + return nil, mockDb, gormDb, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} +} + +func (adapter *SqlmockAdapter) Close() error { + return adapter.mockDb.Close() +} + +type SqlmockAsserter struct { + sqlmockDB *sql.DB + mock sqlmock.Sqlmock +} + +func (a *SqlmockAsserter) Query(query string) Query { + q := a.mock.ExpectQuery(query) + + return &SqlmockQuery{q} +} + +// NewTestHelper returns a fresh TestHelper +func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { + err, mockDb, gormDb, asserter := adapter.Open() + + if err != nil { + return err, nil, nil + } + + return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, asserter: asserter} +} diff --git a/gorm_expect_test.go b/gorm_expect_test.go new file mode 100644 index 00000000..9e726c50 --- /dev/null +++ b/gorm_expect_test.go @@ -0,0 +1,26 @@ +package gorm_test + +import ( + "testing" + + "github.com/iantanwx/gorm" +) + +func TestOpenWithSqlmock(t *testing.T) { + err, _, _ := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) + + if err != nil { + t.Error(err) + } +} + +func TestQuery(t *testing.T) { + err, db, helper := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) + + if err != nil { + t.Fatal(err.Error()) + } + + helper.ExpectFirst(&User{}).Return(&User{}) + db.First(&User{}) +} From 7b6f22713675ffbb4680f3c0b834999260bb4862 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 16 Nov 2017 18:53:54 +0800 Subject: [PATCH 02/39] Add Close method to TestHelper --- gorm_expect.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gorm_expect.go b/gorm_expect.go index 4c29231f..df714201 100644 --- a/gorm_expect.go +++ b/gorm_expect.go @@ -11,9 +11,15 @@ import ( type TestHelper struct { gormDb *DB mockDb *sql.DB + adapter Adapter asserter Asserter } +// Close closes the DB connection +func (h *TestHelper) Close() error { + return h.adapter.Close() +} + func (h *TestHelper) ExpectFirst(model interface{}) Query { fmt.Printf("Expecting query: %s", "some query\n") return h.asserter.Query("some sql") @@ -104,5 +110,5 @@ func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { return err, nil, nil } - return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, asserter: asserter} + return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} } From 85b621bf4e4ff4988721860fbfeee75d4ba5832c Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 16 Nov 2017 18:54:14 +0800 Subject: [PATCH 03/39] Use same helper and mockDb --- gorm_expect_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gorm_expect_test.go b/gorm_expect_test.go index 9e726c50..3d4d37b2 100644 --- a/gorm_expect_test.go +++ b/gorm_expect_test.go @@ -6,21 +6,21 @@ import ( "github.com/iantanwx/gorm" ) +var helper *gorm.TestHelper +var mockDb *gorm.DB + func TestOpenWithSqlmock(t *testing.T) { - err, _, _ := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) + err, db, h := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) if err != nil { - t.Error(err) + t.Fatal(err) } + + helper = h + mockDb = db } func TestQuery(t *testing.T) { - err, db, helper := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) - - if err != nil { - t.Fatal(err.Error()) - } - helper.ExpectFirst(&User{}).Return(&User{}) - db.First(&User{}) + mockDb.First(&User{}) } From eb418da347a40eec696318aea3018c2ac722de09 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 11:44:56 +0800 Subject: [PATCH 04/39] Use github.com/jinzhu/gorm --- gorm_expect_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gorm_expect_test.go b/gorm_expect_test.go index 3d4d37b2..39d3f0c1 100644 --- a/gorm_expect_test.go +++ b/gorm_expect_test.go @@ -3,7 +3,7 @@ package gorm_test import ( "testing" - "github.com/iantanwx/gorm" + "github.com/jinzhu/gorm" ) var helper *gorm.TestHelper From c3e927ba8b1aa9119de89a6ee60238c52a9579dc Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 11:55:19 +0800 Subject: [PATCH 05/39] Reorganise code structure --- gorm_expect.go | 114 --------------------- test_helper.go | 39 +++++++ test_helper_adapter.go | 49 +++++++++ test_helper_assserter.go | 35 +++++++ gorm_expect_test.go => test_helper_test.go | 0 5 files changed, 123 insertions(+), 114 deletions(-) delete mode 100644 gorm_expect.go create mode 100644 test_helper.go create mode 100644 test_helper_adapter.go create mode 100644 test_helper_assserter.go rename gorm_expect_test.go => test_helper_test.go (100%) diff --git a/gorm_expect.go b/gorm_expect.go deleted file mode 100644 index df714201..00000000 --- a/gorm_expect.go +++ /dev/null @@ -1,114 +0,0 @@ -package gorm - -import ( - "database/sql" - "fmt" - - "gopkg.in/DATA-DOG/go-sqlmock.v1" -) - -// TestHelper is the exported struct used for setting expectations -type TestHelper struct { - gormDb *DB - mockDb *sql.DB - adapter Adapter - asserter Asserter -} - -// Close closes the DB connection -func (h *TestHelper) Close() error { - return h.adapter.Close() -} - -func (h *TestHelper) ExpectFirst(model interface{}) Query { - fmt.Printf("Expecting query: %s", "some query\n") - return h.asserter.Query("some sql") -} - -func (h *TestHelper) ExpectFind(model interface{}) { - fmt.Println("Expecting query: %s", "some query involving Find") -} - -type Query interface { - Return(model interface{}) Query -} - -type SqlmockQuery struct { - query *sqlmock.ExpectedQuery -} - -func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { - rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) - rows = rows.AddRow("someval1", "someval2", "someval3") - - return rows -} - -func (q *SqlmockQuery) Return(out interface{}) Query { - rows := q.getRowsForOutType(out) - q.query = q.query.WillReturnRows(rows) - - return q -} - -type Exec interface { - Return(args ...interface{}) -} - -type Adapter interface { - Open() (error, *sql.DB, *DB, Asserter) - Close() error -} - -type Asserter interface { - Query(query string) Query - // Exec(stmt string) Exec -} - -type SqlmockAdapter struct { - mockDb *sql.DB - mock *sqlmock.Sqlmock -} - -// Open returns the raw sql.DB and a gorm DB instance -func (adapter *SqlmockAdapter) Open() (error, *sql.DB, *DB, Asserter) { - mockDb, mock, err := sqlmock.NewWithDSN("mock_gorm_dsn") - - if err != nil { - return err, nil, nil, nil - } - - gormDb, err := Open("sqlmock", "mock_gorm_dsn") - - if err != nil { - return err, nil, nil, nil - } - - return nil, mockDb, gormDb, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} -} - -func (adapter *SqlmockAdapter) Close() error { - return adapter.mockDb.Close() -} - -type SqlmockAsserter struct { - sqlmockDB *sql.DB - mock sqlmock.Sqlmock -} - -func (a *SqlmockAsserter) Query(query string) Query { - q := a.mock.ExpectQuery(query) - - return &SqlmockQuery{q} -} - -// NewTestHelper returns a fresh TestHelper -func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { - err, mockDb, gormDb, asserter := adapter.Open() - - if err != nil { - return err, nil, nil - } - - return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} -} diff --git a/test_helper.go b/test_helper.go new file mode 100644 index 00000000..c1419f11 --- /dev/null +++ b/test_helper.go @@ -0,0 +1,39 @@ +package gorm + +import ( + "database/sql" + "fmt" +) + +// TestHelper is the exported struct used for setting expectations +type TestHelper struct { + gormDb *DB + mockDb *sql.DB + adapter Adapter + asserter Asserter +} + +// Close closes the DB connection +func (h *TestHelper) Close() error { + return h.adapter.Close() +} + +func (h *TestHelper) ExpectFirst(model interface{}) Query { + fmt.Printf("Expecting query: %s", "some query\n") + return h.asserter.Query("some sql") +} + +func (h *TestHelper) ExpectFind(model interface{}) { + fmt.Println("Expecting query: %s", "some query involving Find") +} + +// NewTestHelper returns a fresh TestHelper +func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { + err, mockDb, gormDb, asserter := adapter.Open() + + if err != nil { + return err, nil, nil + } + + return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} +} diff --git a/test_helper_adapter.go b/test_helper_adapter.go new file mode 100644 index 00000000..595edf22 --- /dev/null +++ b/test_helper_adapter.go @@ -0,0 +1,49 @@ +package gorm + +import ( + "database/sql" + + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +type Adapter interface { + Open() (error, *sql.DB, *DB, Asserter) + Close() error +} + +type SqlmockAdapter struct { + mockDb *sql.DB + mock *sqlmock.Sqlmock +} + +// Open returns the raw sql.DB and a gorm DB instance +func (adapter *SqlmockAdapter) Open() (error, *sql.DB, *DB, Asserter) { + mockDb, mock, err := sqlmock.NewWithDSN("mock_gorm_dsn") + + if err != nil { + return err, nil, nil, nil + } + + gormDb, err := Open("sqlmock", "mock_gorm_dsn") + + if err != nil { + return err, nil, nil, nil + } + + return nil, mockDb, gormDb, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} +} + +func (adapter *SqlmockAdapter) Close() error { + return adapter.mockDb.Close() +} + +type SqlmockAsserter struct { + sqlmockDB *sql.DB + mock sqlmock.Sqlmock +} + +func (a *SqlmockAsserter) Query(query string) Query { + q := a.mock.ExpectQuery(query) + + return &SqlmockQuery{q} +} diff --git a/test_helper_assserter.go b/test_helper_assserter.go new file mode 100644 index 00000000..79123b14 --- /dev/null +++ b/test_helper_assserter.go @@ -0,0 +1,35 @@ +package gorm + +import sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" + +type Query interface { + Return(model interface{}) Query +} + +type Exec interface { + Return(args ...interface{}) Exec +} + +type Asserter interface { + Query(query string) Query + // Exec(stmt string) Exec +} + +// SqlmockQuery implements Query for asserter go-sqlmock +type SqlmockQuery struct { + query *sqlmock.ExpectedQuery +} + +func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { + rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) + rows = rows.AddRow("someval1", "someval2", "someval3") + + return rows +} + +func (q *SqlmockQuery) Return(out interface{}) Query { + rows := q.getRowsForOutType(out) + q.query = q.query.WillReturnRows(rows) + + return q +} diff --git a/gorm_expect_test.go b/test_helper_test.go similarity index 100% rename from gorm_expect_test.go rename to test_helper_test.go From f5319a00faf37a42172645edb41cba03d2ab7ce0 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 12:10:24 +0800 Subject: [PATCH 06/39] Add NewDefaultTestHelper func --- test_helper.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test_helper.go b/test_helper.go index c1419f11..a6203554 100644 --- a/test_helper.go +++ b/test_helper.go @@ -27,7 +27,7 @@ func (h *TestHelper) ExpectFind(model interface{}) { fmt.Println("Expecting query: %s", "some query involving Find") } -// NewTestHelper returns a fresh TestHelper +// NewTestHelper returns a fresh TestHelper with an arbitary Adapter func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { err, mockDb, gormDb, asserter := adapter.Open() @@ -37,3 +37,16 @@ func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} } + +// NewDefaultTestHelper returns a TestHelper powered by go-sqlmock +func NewDefaultTestHelper() (error, *DB, *TestHelper) { + adapter := &SqlmockAdapter{} + err, mockDb, gormDb, asserter := adapter.Open() + + if err != nil { + return err, nil, nil + } + + return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} + +} From 36170724dd658b453739a225af8b01d437585bc4 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 12:29:06 +0800 Subject: [PATCH 07/39] Refactor mock connection open/close logic --- test_helper.go | 22 +++++++++++++++------- test_helper_adapter.go | 18 ++++++++---------- test_helper_test.go | 29 +++++++++++++++++++---------- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/test_helper.go b/test_helper.go index a6203554..d9b99118 100644 --- a/test_helper.go +++ b/test_helper.go @@ -1,14 +1,11 @@ package gorm import ( - "database/sql" "fmt" ) // TestHelper is the exported struct used for setting expectations type TestHelper struct { - gormDb *DB - mockDb *sql.DB adapter Adapter asserter Asserter } @@ -29,24 +26,35 @@ func (h *TestHelper) ExpectFind(model interface{}) { // NewTestHelper returns a fresh TestHelper with an arbitary Adapter func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { - err, mockDb, gormDb, asserter := adapter.Open() + err, asserter := adapter.Open() if err != nil { return err, nil, nil } - return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} + gormDb, err := Open("sqlmock", "mock_gorm_dsn") + + if err != nil { + return err, nil, nil + } + + return nil, gormDb, &TestHelper{adapter: adapter, asserter: asserter} } // NewDefaultTestHelper returns a TestHelper powered by go-sqlmock func NewDefaultTestHelper() (error, *DB, *TestHelper) { adapter := &SqlmockAdapter{} - err, mockDb, gormDb, asserter := adapter.Open() + err, asserter := adapter.Open() if err != nil { return err, nil, nil } - return nil, gormDb, &TestHelper{gormDb: gormDb, mockDb: mockDb, adapter: adapter, asserter: asserter} + gormDb, err := Open("sqlmock", "mock_gorm_dsn") + if err != nil { + return err, nil, nil + } + + return nil, gormDb, &TestHelper{adapter: adapter, asserter: asserter} } diff --git a/test_helper_adapter.go b/test_helper_adapter.go index 595edf22..260cd6e3 100644 --- a/test_helper_adapter.go +++ b/test_helper_adapter.go @@ -7,30 +7,28 @@ import ( ) type Adapter interface { - Open() (error, *sql.DB, *DB, Asserter) + Open() (error, Asserter) Close() error } +// SqlmockAdapter implemenets the Adapter interface using go-sqlmock +// it is the default Adapter type SqlmockAdapter struct { mockDb *sql.DB mock *sqlmock.Sqlmock } -// Open returns the raw sql.DB and a gorm DB instance -func (adapter *SqlmockAdapter) Open() (error, *sql.DB, *DB, Asserter) { +// Open returns the raw sql.DB instance and an Asserter +func (adapter *SqlmockAdapter) Open() (error, Asserter) { mockDb, mock, err := sqlmock.NewWithDSN("mock_gorm_dsn") - if err != nil { - return err, nil, nil, nil - } - - gormDb, err := Open("sqlmock", "mock_gorm_dsn") + adapter.mockDb = mockDb if err != nil { - return err, nil, nil, nil + return err, nil } - return nil, mockDb, gormDb, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} + return nil, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} } func (adapter *SqlmockAdapter) Close() error { diff --git a/test_helper_test.go b/test_helper_test.go index 39d3f0c1..f597933b 100644 --- a/test_helper_test.go +++ b/test_helper_test.go @@ -6,21 +6,30 @@ import ( "github.com/jinzhu/gorm" ) -var helper *gorm.TestHelper -var mockDb *gorm.DB +func TestHelperOpen(t *testing.T) { + err, helper, db := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) -func TestOpenWithSqlmock(t *testing.T) { - err, db, h := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) + defer func() { + helper.Close() + db.Close() + }() if err != nil { t.Fatal(err) } - - helper = h - mockDb = db } -func TestQuery(t *testing.T) { - helper.ExpectFirst(&User{}).Return(&User{}) - mockDb.First(&User{}) +func TestHelperClose(t *testing.T) { + err, helper, _ := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) + + closeErr := helper.Close() + + if err != nil { + t.Fatal(closeErr) + } } + +// func TestQuery(t *testing.T) { +// helper.ExpectFirst(&User{}).Return(&User{}) +// mockDb.First(&User{}) +// } From 71174be05ab830db42cc8d823f698bd90660c3a0 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 17:19:49 +0800 Subject: [PATCH 08/39] Rename TestHelper to Expecter and simplify factory function(s) --- test_helper.go | 50 ++++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/test_helper.go b/test_helper.go index d9b99118..b3ee12c9 100644 --- a/test_helper.go +++ b/test_helper.go @@ -4,57 +4,43 @@ import ( "fmt" ) -// TestHelper is the exported struct used for setting expectations -type TestHelper struct { - adapter Adapter - asserter Asserter +// Expecter is the exported struct used for setting expectations +type Expecter struct { + adapter Adapter } -// Close closes the DB connection -func (h *TestHelper) Close() error { - return h.adapter.Close() -} +// AdapterFactory is a generic interface for arbitrary adapters that satisfy +// the interface. variadic args are passed to gorm.Open. +type AdapterFactory func(dialect string, args ...interface{}) (error, *DB, Adapter) -func (h *TestHelper) ExpectFirst(model interface{}) Query { +func (h *Expecter) ExpectFirst(model interface{}) ExpectedQuery { fmt.Printf("Expecting query: %s", "some query\n") - return h.asserter.Query("some sql") + return h.adapter.ExpectQuery("some sql") } -func (h *TestHelper) ExpectFind(model interface{}) { +func (h *Expecter) ExpectFind(model interface{}) ExpectedQuery { fmt.Println("Expecting query: %s", "some query involving Find") + return h.adapter.ExpectQuery("some find condition") } -// NewTestHelper returns a fresh TestHelper with an arbitary Adapter -func NewTestHelper(adapter Adapter) (error, *DB, *TestHelper) { - err, asserter := adapter.Open() +// NewDefaultExpecter returns a Expecter powered by go-sqlmock +func NewDefaultExpecter() (error, *DB, *Expecter) { + err, gormDb, adapter := NewSqlmockAdapter("sqlmock", "mock_gorm_dsn") if err != nil { return err, nil, nil } - gormDb, err := Open("sqlmock", "mock_gorm_dsn") - - if err != nil { - return err, nil, nil - } - - return nil, gormDb, &TestHelper{adapter: adapter, asserter: asserter} + return nil, gormDb, &Expecter{adapter: adapter} } -// NewDefaultTestHelper returns a TestHelper powered by go-sqlmock -func NewDefaultTestHelper() (error, *DB, *TestHelper) { - adapter := &SqlmockAdapter{} - err, asserter := adapter.Open() +// NewExpecter returns an Expecter for arbitrary adapters +func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (error, *DB, *Expecter) { + err, gormDb, adapter := fn(dialect, args...) if err != nil { return err, nil, nil } - gormDb, err := Open("sqlmock", "mock_gorm_dsn") - - if err != nil { - return err, nil, nil - } - - return nil, gormDb, &TestHelper{adapter: adapter, asserter: asserter} + return nil, gormDb, &Expecter{adapter: adapter} } From 27ec78afaaf578a301266f957ba9b5245e3c1b23 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 17:20:42 +0800 Subject: [PATCH 09/39] Remove Asserter interface and combine its functionality into Adapter --- test_helper_adapter.go | 53 +++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/test_helper_adapter.go b/test_helper_adapter.go index 260cd6e3..da5c050f 100644 --- a/test_helper_adapter.go +++ b/test_helper_adapter.go @@ -6,42 +6,53 @@ import ( sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) +var ( + db *sql.DB + mock sqlmock.Sqlmock +) + +func init() { + var err error + + db, mock, err = sqlmock.NewWithDSN("mock_gorm_dsn") + + if err != nil { + panic(err.Error()) + } +} + +// Adapter provides an abstract interface over concrete mock database +// implementations (e.g. go-sqlmock or go-testdb) type Adapter interface { - Open() (error, Asserter) - Close() error + ExpectQuery(stmt string) ExpectedQuery + ExpectExec(stmt string) ExpectedExec } // SqlmockAdapter implemenets the Adapter interface using go-sqlmock // it is the default Adapter type SqlmockAdapter struct { - mockDb *sql.DB - mock *sqlmock.Sqlmock + db *sql.DB + mocker sqlmock.Sqlmock } -// Open returns the raw sql.DB instance and an Asserter -func (adapter *SqlmockAdapter) Open() (error, Asserter) { - mockDb, mock, err := sqlmock.NewWithDSN("mock_gorm_dsn") - - adapter.mockDb = mockDb +func NewSqlmockAdapter(dialect string, args ...interface{}) (error, *DB, Adapter) { + gormDb, err := Open("sqlmock", "mock_gorm_dsn") if err != nil { - return err, nil + return err, nil, nil } - return nil, &SqlmockAsserter{mock: mock, sqlmockDB: mockDb} + return nil, gormDb, &SqlmockAdapter{db: db, mocker: mock} } -func (adapter *SqlmockAdapter) Close() error { - return adapter.mockDb.Close() +func (a *SqlmockAdapter) ExpectQuery(stmt string) ExpectedQuery { + q := a.mocker.ExpectQuery(stmt) + + return &SqlmockQuery{query: q} } -type SqlmockAsserter struct { - sqlmockDB *sql.DB - mock sqlmock.Sqlmock -} +func (a *SqlmockAdapter) ExpectExec(stmt string) ExpectedExec { + e := a.mocker.ExpectExec(stmt) -func (a *SqlmockAsserter) Query(query string) Query { - q := a.mock.ExpectQuery(query) - - return &SqlmockQuery{q} + return &SqlmockExec{exec: e} } From 5505f2ee76d534c788cf56eb305bdd051ea852a2 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 17:24:35 +0800 Subject: [PATCH 10/39] Remove asserter interface --- expecter_result.go | 44 ++++++++++++++++++++++++++++++++++++++++ test_helper_assserter.go | 35 -------------------------------- 2 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 expecter_result.go delete mode 100644 test_helper_assserter.go diff --git a/expecter_result.go b/expecter_result.go new file mode 100644 index 00000000..d8d60095 --- /dev/null +++ b/expecter_result.go @@ -0,0 +1,44 @@ +package gorm + +import ( + "database/sql/driver" + + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +type ExpectedQuery interface { + Returns(model interface{}) ExpectedQuery +} + +type ExpectedExec interface { + Returns(result driver.Result) ExpectedExec +} + +// SqlmockQuery implements Query for asserter go-sqlmock +type SqlmockQuery struct { + query *sqlmock.ExpectedQuery +} + +func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { + rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) + rows = rows.AddRow("someval1", "someval2", "someval3") + + return rows +} + +func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { + rows := q.getRowsForOutType(out) + q.query = q.query.WillReturnRows(rows) + + return q +} + +type SqlmockExec struct { + exec *sqlmock.ExpectedExec +} + +func (e *SqlmockExec) Returns(result driver.Result) ExpectedExec { + e.exec = e.exec.WillReturnResult(result) + + return e +} diff --git a/test_helper_assserter.go b/test_helper_assserter.go deleted file mode 100644 index 79123b14..00000000 --- a/test_helper_assserter.go +++ /dev/null @@ -1,35 +0,0 @@ -package gorm - -import sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" - -type Query interface { - Return(model interface{}) Query -} - -type Exec interface { - Return(args ...interface{}) Exec -} - -type Asserter interface { - Query(query string) Query - // Exec(stmt string) Exec -} - -// SqlmockQuery implements Query for asserter go-sqlmock -type SqlmockQuery struct { - query *sqlmock.ExpectedQuery -} - -func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { - rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) - rows = rows.AddRow("someval1", "someval2", "someval3") - - return rows -} - -func (q *SqlmockQuery) Return(out interface{}) Query { - rows := q.getRowsForOutType(out) - q.query = q.query.WillReturnRows(rows) - - return q -} From b9fc77cb771cce2104b8600d3b429cd08939c6d9 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 17:24:57 +0800 Subject: [PATCH 11/39] Rename files --- test_helper.go => expecter.go | 0 test_helper_adapter.go => expecter_adapter.go | 0 expecter_test.go | 43 +++++++++++++++++++ test_helper_test.go | 35 --------------- 4 files changed, 43 insertions(+), 35 deletions(-) rename test_helper.go => expecter.go (100%) rename test_helper_adapter.go => expecter_adapter.go (100%) create mode 100644 expecter_test.go delete mode 100644 test_helper_test.go diff --git a/test_helper.go b/expecter.go similarity index 100% rename from test_helper.go rename to expecter.go diff --git a/test_helper_adapter.go b/expecter_adapter.go similarity index 100% rename from test_helper_adapter.go rename to expecter_adapter.go diff --git a/expecter_test.go b/expecter_test.go new file mode 100644 index 00000000..d6b5abea --- /dev/null +++ b/expecter_test.go @@ -0,0 +1,43 @@ +package gorm_test + +import ( + "testing" + + "github.com/jinzhu/gorm" +) + +func TestNewDefaultExpecter(t *testing.T) { + err, db, _ := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } +} + +func TestNewCustomExpecter(t *testing.T) { + err, db, _ := gorm.NewExpecter(gorm.NewSqlmockAdapter, "sqlmock", "mock_gorm_dsn") + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } +} + +func TestQuery(t *testing.T) { + err, db, expect := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + expect.ExpectFirst(&User{}).Returns(&User{}) + db.First(&User{}) +} diff --git a/test_helper_test.go b/test_helper_test.go deleted file mode 100644 index f597933b..00000000 --- a/test_helper_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package gorm_test - -import ( - "testing" - - "github.com/jinzhu/gorm" -) - -func TestHelperOpen(t *testing.T) { - err, helper, db := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) - - defer func() { - helper.Close() - db.Close() - }() - - if err != nil { - t.Fatal(err) - } -} - -func TestHelperClose(t *testing.T) { - err, helper, _ := gorm.NewTestHelper(&gorm.SqlmockAdapter{}) - - closeErr := helper.Close() - - if err != nil { - t.Fatal(closeErr) - } -} - -// func TestQuery(t *testing.T) { -// helper.ExpectFirst(&User{}).Return(&User{}) -// mockDb.First(&User{}) -// } From 90528be927cb26c288df2e8a32e5aec80e7f028b Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 17 Nov 2017 18:29:31 +0800 Subject: [PATCH 12/39] Implement private clone method to support fluent API --- expecter.go | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/expecter.go b/expecter.go index b3ee12c9..057377e7 100644 --- a/expecter.go +++ b/expecter.go @@ -4,23 +4,19 @@ import ( "fmt" ) -// Expecter is the exported struct used for setting expectations -type Expecter struct { - adapter Adapter -} - // AdapterFactory is a generic interface for arbitrary adapters that satisfy // the interface. variadic args are passed to gorm.Open. type AdapterFactory func(dialect string, args ...interface{}) (error, *DB, Adapter) -func (h *Expecter) ExpectFirst(model interface{}) ExpectedQuery { - fmt.Printf("Expecting query: %s", "some query\n") - return h.adapter.ExpectQuery("some sql") -} +// Expecter is the exported struct used for setting expectations +type Expecter struct { + Value interface{} + adapter Adapter + search *search + values map[string]interface{} -func (h *Expecter) ExpectFind(model interface{}) ExpectedQuery { - fmt.Println("Expecting query: %s", "some query involving Find") - return h.adapter.ExpectQuery("some find condition") + // globally scoped expecter + root *Expecter } // NewDefaultExpecter returns a Expecter powered by go-sqlmock @@ -44,3 +40,30 @@ func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (error, return nil, gormDb, &Expecter{adapter: adapter} } + +/* PUBLIC METHODS */ + +func (h *Expecter) ExpectFirst(model interface{}) ExpectedQuery { + fmt.Printf("Expecting query: %s", "some query\n") + return h.adapter.ExpectQuery("some sql") +} + +func (h *Expecter) ExpectFind(model interface{}) ExpectedQuery { + fmt.Println("Expecting query: %s", "some query involving Find") + return h.adapter.ExpectQuery("some find condition") +} + +/* PRIVATE METHODS */ + +// clone is similar to DB.clone, and ensures that the root Expecter is not +// polluted with subsequent search constraints +func (h *Expecter) clone() *Expecter { + expecterCopy := &Expecter{ + adapter: h.adapter, + root: h.root, + values: map[string]interface{}{}, + Value: h.Value, + } + + return expecterCopy +} From 5520530799d1d32baeb99f2132c6d21b2ce2ca79 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sat, 18 Nov 2017 16:37:14 +0800 Subject: [PATCH 13/39] Fix some go lint errors --- expecter.go | 26 ++++++++++++++------------ expecter_adapter.go | 6 +++--- expecter_test.go | 8 ++++---- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/expecter.go b/expecter.go index 057377e7..d95b203e 100644 --- a/expecter.go +++ b/expecter.go @@ -6,7 +6,7 @@ import ( // AdapterFactory is a generic interface for arbitrary adapters that satisfy // the interface. variadic args are passed to gorm.Open. -type AdapterFactory func(dialect string, args ...interface{}) (error, *DB, Adapter) +type AdapterFactory func(dialect string, args ...interface{}) (*DB, Adapter, error) // Expecter is the exported struct used for setting expectations type Expecter struct { @@ -20,36 +20,38 @@ type Expecter struct { } // NewDefaultExpecter returns a Expecter powered by go-sqlmock -func NewDefaultExpecter() (error, *DB, *Expecter) { - err, gormDb, adapter := NewSqlmockAdapter("sqlmock", "mock_gorm_dsn") +func NewDefaultExpecter() (*DB, *Expecter, error) { + gormDb, adapter, err := NewSqlmockAdapter("sqlmock", "mock_gorm_dsn") if err != nil { - return err, nil, nil + return nil, nil, err } - return nil, gormDb, &Expecter{adapter: adapter} + return gormDb, &Expecter{adapter: adapter}, nil } // NewExpecter returns an Expecter for arbitrary adapters -func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (error, *DB, *Expecter) { - err, gormDb, adapter := fn(dialect, args...) +func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (*DB, *Expecter, error) { + gormDb, adapter, err := fn(dialect, args...) if err != nil { - return err, nil, nil + return nil, nil, err } - return nil, gormDb, &Expecter{adapter: adapter} + return gormDb, &Expecter{adapter: adapter}, nil } /* PUBLIC METHODS */ -func (h *Expecter) ExpectFirst(model interface{}) ExpectedQuery { +// First triggers a Query +func (h *Expecter) First(model interface{}) ExpectedQuery { fmt.Printf("Expecting query: %s", "some query\n") return h.adapter.ExpectQuery("some sql") } -func (h *Expecter) ExpectFind(model interface{}) ExpectedQuery { - fmt.Println("Expecting query: %s", "some query involving Find") +// Find triggers a Query +func (h *Expecter) Find(model interface{}) ExpectedQuery { + fmt.Printf("Expecting query: %s\n", "some query involving Find") return h.adapter.ExpectQuery("some find condition") } diff --git a/expecter_adapter.go b/expecter_adapter.go index da5c050f..08b618c8 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -35,14 +35,14 @@ type SqlmockAdapter struct { mocker sqlmock.Sqlmock } -func NewSqlmockAdapter(dialect string, args ...interface{}) (error, *DB, Adapter) { +func NewSqlmockAdapter(dialect string, args ...interface{}) (*DB, Adapter, error) { gormDb, err := Open("sqlmock", "mock_gorm_dsn") if err != nil { - return err, nil, nil + return nil, nil, err } - return nil, gormDb, &SqlmockAdapter{db: db, mocker: mock} + return gormDb, &SqlmockAdapter{db: db, mocker: mock}, nil } func (a *SqlmockAdapter) ExpectQuery(stmt string) ExpectedQuery { diff --git a/expecter_test.go b/expecter_test.go index d6b5abea..eeb24c48 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -7,7 +7,7 @@ import ( ) func TestNewDefaultExpecter(t *testing.T) { - err, db, _ := gorm.NewDefaultExpecter() + db, _, err := gorm.NewDefaultExpecter() defer func() { db.Close() }() @@ -18,7 +18,7 @@ func TestNewDefaultExpecter(t *testing.T) { } func TestNewCustomExpecter(t *testing.T) { - err, db, _ := gorm.NewExpecter(gorm.NewSqlmockAdapter, "sqlmock", "mock_gorm_dsn") + db, _, err := gorm.NewExpecter(gorm.NewSqlmockAdapter, "sqlmock", "mock_gorm_dsn") defer func() { db.Close() }() @@ -29,7 +29,7 @@ func TestNewCustomExpecter(t *testing.T) { } func TestQuery(t *testing.T) { - err, db, expect := gorm.NewDefaultExpecter() + db, expect, err := gorm.NewDefaultExpecter() defer func() { db.Close() }() @@ -38,6 +38,6 @@ func TestQuery(t *testing.T) { t.Fatal(err) } - expect.ExpectFirst(&User{}).Returns(&User{}) + expect.First(&User{}).Returns(&User{}) db.First(&User{}) } From 475ba3db3e23d3675dba047328e9e8df1d7308d5 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sat, 18 Nov 2017 19:06:00 +0800 Subject: [PATCH 14/39] Proof of concept using DB.logger --- expecter.go | 113 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/expecter.go b/expecter.go index d95b203e..a6bb3578 100644 --- a/expecter.go +++ b/expecter.go @@ -1,22 +1,84 @@ package gorm import ( + "database/sql" + "errors" "fmt" ) +type Recorder struct { + stmt string +} + +func (r *Recorder) Print(args ...interface{}) { + msgs := LogFormatter(args...) + if len(msgs) >= 4 { + if v, ok := msgs[3].(string); ok { + r.stmt = v + } + // for i, msg := range msgs { + // switch v := msg.(type) { + // case string: + // case []byte: + // r.buf.Read([]byte(v)) + // default: + // return + // } + // } + } +} + // AdapterFactory is a generic interface for arbitrary adapters that satisfy // the interface. variadic args are passed to gorm.Open. type AdapterFactory func(dialect string, args ...interface{}) (*DB, Adapter, error) // Expecter is the exported struct used for setting expectations type Expecter struct { - Value interface{} - adapter Adapter - search *search - values map[string]interface{} - // globally scoped expecter - root *Expecter + adapter Adapter + noop NoopDB + gorm *DB + recorder *Recorder +} + +type NoopDB interface { + GetStmts() []string +} + +type DefaultNoopDB struct{} + +type NoopResult struct{} + +func (r NoopResult) LastInsertId() (int64, error) { + return 1, nil +} + +func (r NoopResult) RowsAffected() (int64, error) { + return 1, nil +} + +func NewNoopDB() NoopDB { + return &DefaultNoopDB{} +} + +func (r *DefaultNoopDB) Exec(query string, args ...interface{}) (sql.Result, error) { + return NoopResult{}, nil +} + +func (r *DefaultNoopDB) Prepare(query string) (*sql.Stmt, error) { + return &sql.Stmt{}, nil +} + +func (r *DefaultNoopDB) Query(query string, args ...interface{}) (*sql.Rows, error) { + return nil, errors.New("noop") +} + +func (r *DefaultNoopDB) QueryRow(query string, args ...interface{}) *sql.Row { + return &sql.Row{} +} + +func (r *DefaultNoopDB) GetStmts() []string { + return []string{"not", "implemented"} } // NewDefaultExpecter returns a Expecter powered by go-sqlmock @@ -27,7 +89,21 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { return nil, nil, err } - return gormDb, &Expecter{adapter: adapter}, nil + recorder := &Recorder{} + noop := &DefaultNoopDB{} + + gorm := &DB{ + db: noop, + logger: recorder, + logMode: 2, + values: map[string]interface{}{}, + callbacks: DefaultCallback, + dialect: newDialect("sqlmock", noop), + } + + gorm.parent = gorm + + return gormDb, &Expecter{adapter: adapter, noop: noop, gorm: gorm, recorder: recorder}, nil } // NewExpecter returns an Expecter for arbitrary adapters @@ -44,28 +120,13 @@ func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (*DB, * /* PUBLIC METHODS */ // First triggers a Query -func (h *Expecter) First(model interface{}) ExpectedQuery { - fmt.Printf("Expecting query: %s", "some query\n") - return h.adapter.ExpectQuery("some sql") +func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { + h.gorm.First(out, where) + return h.adapter.ExpectQuery(h.recorder.stmt) } // Find triggers a Query -func (h *Expecter) Find(model interface{}) ExpectedQuery { +func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { fmt.Printf("Expecting query: %s\n", "some query involving Find") return h.adapter.ExpectQuery("some find condition") } - -/* PRIVATE METHODS */ - -// clone is similar to DB.clone, and ensures that the root Expecter is not -// polluted with subsequent search constraints -func (h *Expecter) clone() *Expecter { - expecterCopy := &Expecter{ - adapter: h.adapter, - root: h.root, - values: map[string]interface{}{}, - Value: h.Value, - } - - return expecterCopy -} From ae219db84b4c36df0f332d9c9ad69d5de6452996 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sat, 18 Nov 2017 19:11:21 +0800 Subject: [PATCH 15/39] Add method to assert all expectations --- expecter.go | 13 ++++--------- expecter_adapter.go | 5 +++++ expecter_test.go | 5 ++++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/expecter.go b/expecter.go index a6bb3578..f6153c84 100644 --- a/expecter.go +++ b/expecter.go @@ -16,15 +16,6 @@ func (r *Recorder) Print(args ...interface{}) { if v, ok := msgs[3].(string); ok { r.stmt = v } - // for i, msg := range msgs { - // switch v := msg.(type) { - // case string: - // case []byte: - // r.buf.Read([]byte(v)) - // default: - // return - // } - // } } } @@ -119,6 +110,10 @@ func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (*DB, * /* PUBLIC METHODS */ +func (h *Expecter) AssertExpectations() error { + return h.adapter.AssertExpectations() +} + // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { h.gorm.First(out, where) diff --git a/expecter_adapter.go b/expecter_adapter.go index 08b618c8..8e3025db 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -26,6 +26,7 @@ func init() { type Adapter interface { ExpectQuery(stmt string) ExpectedQuery ExpectExec(stmt string) ExpectedExec + AssertExpectations() error } // SqlmockAdapter implemenets the Adapter interface using go-sqlmock @@ -56,3 +57,7 @@ func (a *SqlmockAdapter) ExpectExec(stmt string) ExpectedExec { return &SqlmockExec{exec: e} } + +func (a *SqlmockAdapter) AssertExpectations() error { + return a.mocker.ExpectationsWereMet() +} diff --git a/expecter_test.go b/expecter_test.go index eeb24c48..edef7e06 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -38,6 +38,9 @@ func TestQuery(t *testing.T) { t.Fatal(err) } - expect.First(&User{}).Returns(&User{}) + expect.First(&User{}) + db.First(&User{}) + + expect.AssertExpectations() } From 6a1b81b18b0c82cbbe00c15f02dc3db5d2f97b1f Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sat, 18 Nov 2017 19:24:45 +0800 Subject: [PATCH 16/39] Escape query string and assert err == nil --- expecter.go | 5 +++-- expecter_test.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/expecter.go b/expecter.go index f6153c84..ebe36269 100644 --- a/expecter.go +++ b/expecter.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "regexp" ) type Recorder struct { @@ -116,8 +117,8 @@ func (h *Expecter) AssertExpectations() error { // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { - h.gorm.First(out, where) - return h.adapter.ExpectQuery(h.recorder.stmt) + h.gorm.First(out) + return h.adapter.ExpectQuery(regexp.QuoteMeta(h.recorder.stmt)) } // Find triggers a Query diff --git a/expecter_test.go b/expecter_test.go index edef7e06..c93bd06f 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -39,8 +39,9 @@ func TestQuery(t *testing.T) { } expect.First(&User{}) - db.First(&User{}) - expect.AssertExpectations() + if err := expect.AssertExpectations(); err != nil { + t.Error(err) + } } From 8c06f3ff4f92ce7ff2e3836fc7e58edcc6832330 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sat, 18 Nov 2017 21:23:55 +0800 Subject: [PATCH 17/39] Fix variadic arguments --- expecter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/expecter.go b/expecter.go index ebe36269..87b716a7 100644 --- a/expecter.go +++ b/expecter.go @@ -117,7 +117,7 @@ func (h *Expecter) AssertExpectations() error { // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { - h.gorm.First(out) + h.gorm.First(out, where...) return h.adapter.ExpectQuery(regexp.QuoteMeta(h.recorder.stmt)) } From 62740e8e1ecb7cd1673fd65a62c0ffd4dd7e21ab Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Mon, 20 Nov 2017 10:25:12 +0800 Subject: [PATCH 18/39] Add docs --- expecter.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/expecter.go b/expecter.go index 87b716a7..c18a7f55 100644 --- a/expecter.go +++ b/expecter.go @@ -7,10 +7,13 @@ import ( "regexp" ) +// Recorder satisfies the logger interface type Recorder struct { stmt string } +// Print just sets the last recorded SQL statement +// TODO: find a better way to extract SQL from log messages func (r *Recorder) Print(args ...interface{}) { msgs := LogFormatter(args...) if len(msgs) >= 4 { @@ -28,17 +31,15 @@ type AdapterFactory func(dialect string, args ...interface{}) (*DB, Adapter, err type Expecter struct { // globally scoped expecter adapter Adapter - noop NoopDB + noop SQLCommon gorm *DB recorder *Recorder } -type NoopDB interface { - GetStmts() []string -} - +// DefaultNoopDB is a noop db used to get generated sql from gorm.DB type DefaultNoopDB struct{} +// NoopResult is a noop struct that satisfies sql.Result type NoopResult struct{} func (r NoopResult) LastInsertId() (int64, error) { @@ -49,30 +50,31 @@ func (r NoopResult) RowsAffected() (int64, error) { return 1, nil } -func NewNoopDB() NoopDB { +// NewNoopDB initialises a new DefaultNoopDB +func NewNoopDB() SQLCommon { return &DefaultNoopDB{} } +// Exec simulates a sql.DB.Exec func (r *DefaultNoopDB) Exec(query string, args ...interface{}) (sql.Result, error) { return NoopResult{}, nil } +// Prepare simulates a sql.DB.Prepare func (r *DefaultNoopDB) Prepare(query string) (*sql.Stmt, error) { return &sql.Stmt{}, nil } +// Query simulates a sql.DB.Query func (r *DefaultNoopDB) Query(query string, args ...interface{}) (*sql.Rows, error) { return nil, errors.New("noop") } +// QueryRow simulates a sql.DB.QueryRow func (r *DefaultNoopDB) QueryRow(query string, args ...interface{}) *sql.Row { return &sql.Row{} } -func (r *DefaultNoopDB) GetStmts() []string { - return []string{"not", "implemented"} -} - // NewDefaultExpecter returns a Expecter powered by go-sqlmock func NewDefaultExpecter() (*DB, *Expecter, error) { gormDb, adapter, err := NewSqlmockAdapter("sqlmock", "mock_gorm_dsn") @@ -83,7 +85,6 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { recorder := &Recorder{} noop := &DefaultNoopDB{} - gorm := &DB{ db: noop, logger: recorder, @@ -111,6 +112,7 @@ func NewExpecter(fn AdapterFactory, dialect string, args ...interface{}) (*DB, * /* PUBLIC METHODS */ +// AssertExpectations checks if all expected Querys and Execs were satisfied. func (h *Expecter) AssertExpectations() error { return h.adapter.AssertExpectations() } From a4ba25028d95f885dcd5c399577f04d2dc413b29 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Mon, 20 Nov 2017 16:02:40 +0800 Subject: [PATCH 19/39] Add Value() method to gorm_test.Num to prevent scan error --- migration_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/migration_test.go b/migration_test.go index 3f3a5c8f..6c10f62e 100644 --- a/migration_test.go +++ b/migration_test.go @@ -139,6 +139,11 @@ func (role Role) IsAdmin() bool { type Num int64 +func (i *Num) Value() (driver.Value, error) { + // guaranteed ok + return int64(*i), nil +} + func (i *Num) Scan(src interface{}) error { switch s := src.(type) { case []byte: From b4591d4db84096705bd842b389cfd44a6eca8cd3 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Mon, 20 Nov 2017 16:04:38 +0800 Subject: [PATCH 20/39] Add naive get rows function --- expecter.go | 33 ++++++++++++++++++++++++++++----- expecter_result.go | 35 ++++++++++++++++++++++++++++++++--- expecter_test.go | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/expecter.go b/expecter.go index c18a7f55..6ded475e 100644 --- a/expecter.go +++ b/expecter.go @@ -12,14 +12,37 @@ type Recorder struct { stmt string } +type Stmt struct { + stmtType string + sql string + args []interface{} +} + +func getStmtFromLog(values ...interface{}) Stmt { + var statement Stmt + + if len(values) > 1 { + var ( + level = values[0] + ) + + if level == "sql" { + statement.args = values[4].([]interface{}) + statement.sql = values[3].(string) + } + + return statement + } + + return statement +} + // Print just sets the last recorded SQL statement // TODO: find a better way to extract SQL from log messages func (r *Recorder) Print(args ...interface{}) { - msgs := LogFormatter(args...) - if len(msgs) >= 4 { - if v, ok := msgs[3].(string); ok { - r.stmt = v - } + statement := getStmtFromLog(args...) + if statement.sql != "" { + r.stmt = statement.sql } } diff --git a/expecter_result.go b/expecter_result.go index d8d60095..80bc8ed7 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -16,12 +16,40 @@ type ExpectedExec interface { // SqlmockQuery implements Query for asserter go-sqlmock type SqlmockQuery struct { + scope *Scope query *sqlmock.ExpectedQuery } func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { - rows := sqlmock.NewRows([]string{"column1", "column2", "column3"}) - rows = rows.AddRow("someval1", "someval2", "someval3") + var ( + columns []string + rows *sqlmock.Rows + values []driver.Value + ) + + q.scope = &Scope{Value: out} + fields := q.scope.Fields() + + for _, field := range fields { + if field.IsNormal { + var ( + column = field.StructField.DBName + value = field.Field.Interface() + ) + + if isValue := driver.IsValue(value); isValue { + columns = append(columns, column) + values = append(values, value) + } else if valuer, ok := value.(driver.Valuer); ok { + if underlyingValue, err := valuer.Value(); err == nil { + values = append(values, underlyingValue) + columns = append(columns, field.StructField.DBName) + } + } + } + } + + rows = sqlmock.NewRows(columns).AddRow(values...) return rows } @@ -34,7 +62,8 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { } type SqlmockExec struct { - exec *sqlmock.ExpectedExec + scope *Scope + exec *sqlmock.ExpectedExec } func (e *SqlmockExec) Returns(result driver.Result) ExpectedExec { diff --git a/expecter_test.go b/expecter_test.go index c93bd06f..cb77e01c 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -1,6 +1,7 @@ package gorm_test import ( + "reflect" "testing" "github.com/jinzhu/gorm" @@ -39,9 +40,39 @@ func TestQuery(t *testing.T) { } expect.First(&User{}) - db.First(&User{}) + db.LogMode(true).First(&User{}) if err := expect.AssertExpectations(); err != nil { t.Error(err) } } + +func TestQueryReturn(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := &User{Id: 1} + expectedOut := User{Id: 1, Name: "jinzhu"} + + expect.First(in).Returns(User{Id: 1, Name: "jinzhu"}) + + db.First(in) + + if e := expect.AssertExpectations(); e != nil { + t.Error(e) + } + + if in.Name != "jinzhu" { + t.Errorf("Expected %s, got %s", expectedOut.Name, in.Name) + } + + if ne := reflect.DeepEqual(*in, expectedOut); !ne { + t.Errorf("Not equal") + } +} From afe269ec7db06ca826e800911ffa68b015d7ac23 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Mon, 20 Nov 2017 18:38:57 +0800 Subject: [PATCH 21/39] Support slice value -> sql.Rows --- expecter.go | 5 ++- expecter_result.go | 87 ++++++++++++++++++++++++++++++++++------------ expecter_test.go | 57 ++++++++++++++++++++++++++---- migration_test.go | 4 +-- 4 files changed, 120 insertions(+), 33 deletions(-) diff --git a/expecter.go b/expecter.go index 6ded475e..7251dd3a 100644 --- a/expecter.go +++ b/expecter.go @@ -3,7 +3,6 @@ package gorm import ( "database/sql" "errors" - "fmt" "regexp" ) @@ -148,6 +147,6 @@ func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { // Find triggers a Query func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { - fmt.Printf("Expecting query: %s\n", "some query involving Find") - return h.adapter.ExpectQuery("some find condition") + h.gorm.Find(out, where...) + return h.adapter.ExpectQuery(regexp.QuoteMeta(h.recorder.stmt)) } diff --git a/expecter_result.go b/expecter_result.go index 80bc8ed7..1339c195 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -2,7 +2,10 @@ package gorm import ( "database/sql/driver" + "fmt" + "reflect" + "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -20,36 +23,76 @@ type SqlmockQuery struct { query *sqlmock.ExpectedQuery } -func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { - var ( - columns []string - rows *sqlmock.Rows - values []driver.Value - ) - - q.scope = &Scope{Value: out} - fields := q.scope.Fields() - +func getRowForFields(fields []*Field) []driver.Value { + var values []driver.Value for _, field := range fields { if field.IsNormal { - var ( - column = field.StructField.DBName - value = field.Field.Interface() - ) + value := field.Field - if isValue := driver.IsValue(value); isValue { - columns = append(columns, column) - values = append(values, value) - } else if valuer, ok := value.(driver.Valuer); ok { - if underlyingValue, err := valuer.Value(); err == nil { - values = append(values, underlyingValue) - columns = append(columns, field.StructField.DBName) + // dereference pointers + if field.Field.Kind() == reflect.Ptr { + value = reflect.Indirect(field.Field) + } + + // check if we have a zero Value + if !value.IsValid() { + values = append(values, nil) + continue + } + + concreteVal := value.Interface() + + // if we already have a driver.Value, just append + _, isValuer := concreteVal.(driver.Valuer) + spew.Printf("%s: %v\r\n", field.DBName, isValuer) + + if driver.IsValue(concreteVal) { + values = append(values, concreteVal) + } else if valuer, ok := concreteVal.(driver.Valuer); ok { + if convertedValue, err := valuer.Value(); err == nil { + values = append(values, convertedValue) } } } } - rows = sqlmock.NewRows(columns).AddRow(values...) + return values +} + +func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { + var columns []string + + for _, field := range (&Scope{}).New(out).GetModelStruct().StructFields { + if field.IsNormal { + columns = append(columns, field.DBName) + } + } + + rows := sqlmock.NewRows(columns) + + outVal := indirect(reflect.ValueOf(out)) + + if outVal.Kind() == reflect.Slice { + outSlice := []interface{}{} + for i := 0; i < outVal.Len(); i++ { + outSlice = append(outSlice, outVal.Index(i).Interface()) + } + + for _, outElem := range outSlice { + scope := &Scope{Value: outElem} + row := getRowForFields(scope.Fields()) + rows = rows.AddRow(row...) + } + } else if outVal.Kind() == reflect.Struct { + scope := &Scope{Value: out} + row := getRowForFields(scope.Fields()) + rows = rows.AddRow(row...) + } else { + panic(fmt.Errorf("Can only get rows for slice or struct")) + } + + spew.Dump(columns) + spew.Dump(rows) return rows } diff --git a/expecter_test.go b/expecter_test.go index cb77e01c..ba8cb8b7 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -57,22 +57,67 @@ func TestQueryReturn(t *testing.T) { t.Fatal(err) } - in := &User{Id: 1} - expectedOut := User{Id: 1, Name: "jinzhu"} + in := User{Id: 1} + out := User{Id: 1, Name: "jinzhu"} - expect.First(in).Returns(User{Id: 1, Name: "jinzhu"}) + expect.First(&in).Returns(out) - db.First(in) + db.First(&in) if e := expect.AssertExpectations(); e != nil { t.Error(e) } if in.Name != "jinzhu" { - t.Errorf("Expected %s, got %s", expectedOut.Name, in.Name) + t.Errorf("Expected %s, got %s", out.Name, in.Name) } - if ne := reflect.DeepEqual(*in, expectedOut); !ne { + if ne := reflect.DeepEqual(in, out); !ne { t.Errorf("Not equal") } } + +func TestFindStructDest(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := &User{Id: 1} + + expect.Find(in) + db.Find(&User{Id: 1}) + + if e := expect.AssertExpectations(); e != nil { + t.Error(e) + } +} + +func TestFindSlice(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := []User{} + out := []User{User{Id: 1, Name: "jinzhu"}, User{Id: 2, Name: "itwx"}} + + expect.Find(&in).Returns(&out) + db.Find(&in) + + if e := expect.AssertExpectations(); e != nil { + t.Error(e) + } + + if ne := reflect.DeepEqual(in, out); !ne { + t.Error("Expected equal slices") + } +} diff --git a/migration_test.go b/migration_test.go index 6c10f62e..0d389268 100644 --- a/migration_test.go +++ b/migration_test.go @@ -139,9 +139,9 @@ func (role Role) IsAdmin() bool { type Num int64 -func (i *Num) Value() (driver.Value, error) { +func (i Num) Value() (driver.Value, error) { // guaranteed ok - return int64(*i), nil + return int64(i), nil } func (i *Num) Scan(src interface{}) error { From e9a29091fa6ab391eeb151899185dcc73908455c Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Mon, 20 Nov 2017 18:41:06 +0800 Subject: [PATCH 22/39] Clean up debug code --- expecter_result.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/expecter_result.go b/expecter_result.go index 1339c195..16a2a1e0 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" - "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -19,7 +18,6 @@ type ExpectedExec interface { // SqlmockQuery implements Query for asserter go-sqlmock type SqlmockQuery struct { - scope *Scope query *sqlmock.ExpectedQuery } @@ -35,6 +33,7 @@ func getRowForFields(fields []*Field) []driver.Value { } // check if we have a zero Value + // just append nil if it's not valid, so sqlmock won't complain if !value.IsValid() { values = append(values, nil) continue @@ -42,10 +41,6 @@ func getRowForFields(fields []*Field) []driver.Value { concreteVal := value.Interface() - // if we already have a driver.Value, just append - _, isValuer := concreteVal.(driver.Valuer) - spew.Printf("%s: %v\r\n", field.DBName, isValuer) - if driver.IsValue(concreteVal) { values = append(values, concreteVal) } else if valuer, ok := concreteVal.(driver.Valuer); ok { @@ -91,9 +86,6 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { panic(fmt.Errorf("Can only get rows for slice or struct")) } - spew.Dump(columns) - spew.Dump(rows) - return rows } @@ -105,8 +97,7 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { } type SqlmockExec struct { - scope *Scope - exec *sqlmock.ExpectedExec + exec *sqlmock.ExpectedExec } func (e *SqlmockExec) Returns(result driver.Result) ExpectedExec { From 486fb73ee5044f5e3fde8a9d534d078e6a89a881 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Tue, 21 Nov 2017 12:26:18 +0800 Subject: [PATCH 23/39] Add documentation to prevent go lint from complaining --- expecter.go | 3 +++ expecter_adapter.go | 9 +++++++++ expecter_result.go | 15 ++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/expecter.go b/expecter.go index 7251dd3a..1dab967c 100644 --- a/expecter.go +++ b/expecter.go @@ -11,6 +11,7 @@ type Recorder struct { stmt string } +// Stmt represents a sql statement. It can be an Exec or Query type Stmt struct { stmtType string sql string @@ -64,10 +65,12 @@ type DefaultNoopDB struct{} // NoopResult is a noop struct that satisfies sql.Result type NoopResult struct{} +// LastInsertId is a noop method for satisfying drive.Result func (r NoopResult) LastInsertId() (int64, error) { return 1, nil } +// RowsAffected is a noop method for satisfying drive.Result func (r NoopResult) RowsAffected() (int64, error) { return 1, nil } diff --git a/expecter_adapter.go b/expecter_adapter.go index 8e3025db..7e3d3da9 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -36,6 +36,8 @@ type SqlmockAdapter struct { mocker sqlmock.Sqlmock } +// NewSqlmockAdapter returns a mock gorm.DB and an Adapter backed by +// go-sqlmock func NewSqlmockAdapter(dialect string, args ...interface{}) (*DB, Adapter, error) { gormDb, err := Open("sqlmock", "mock_gorm_dsn") @@ -46,18 +48,25 @@ func NewSqlmockAdapter(dialect string, args ...interface{}) (*DB, Adapter, error return gormDb, &SqlmockAdapter{db: db, mocker: mock}, nil } +// ExpectQuery wraps the underlying mock method for setting a query +// expectation func (a *SqlmockAdapter) ExpectQuery(stmt string) ExpectedQuery { q := a.mocker.ExpectQuery(stmt) return &SqlmockQuery{query: q} } +// ExpectExec wraps the underlying mock method for setting a exec +// expectation func (a *SqlmockAdapter) ExpectExec(stmt string) ExpectedExec { e := a.mocker.ExpectExec(stmt) return &SqlmockExec{exec: e} } +// AssertExpectations asserts that _all_ expectations for a test have been met +// and returns an error specifying which have not if there are unmet +// expectations func (a *SqlmockAdapter) AssertExpectations() error { return a.mocker.ExpectationsWereMet() } diff --git a/expecter_result.go b/expecter_result.go index 16a2a1e0..0f2ecc6e 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -8,15 +8,21 @@ import ( sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) +// ExpectedQuery represents an expected query that will be executed and can +// return some rows. It presents a fluent API for chaining calls to other +// expectations type ExpectedQuery interface { Returns(model interface{}) ExpectedQuery } +// ExpectedExec represents an expected exec that will be executed and can +// return a result. It presents a fluent API for chaining calls to other +// expectations type ExpectedExec interface { Returns(result driver.Result) ExpectedExec } -// SqlmockQuery implements Query for asserter go-sqlmock +// SqlmockQuery implements Query for go-sqlmock type SqlmockQuery struct { query *sqlmock.ExpectedQuery } @@ -89,6 +95,9 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { return rows } +// Returns accepts an out type which should either be a struct or slice. Under +// the hood, it converts a gorm model struct to sql.Rows that can be passed to +// the underlying mock db func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { rows := q.getRowsForOutType(out) q.query = q.query.WillReturnRows(rows) @@ -96,10 +105,14 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { return q } +// SqlmockExec implements Exec for go-sqlmock type SqlmockExec struct { exec *sqlmock.ExpectedExec } +// Returns accepts a driver.Result. It is passed directly to the underlying +// mock db. Useful for checking DAO behaviour in the event that the incorrect +// number of rows are affected by an Exec func (e *SqlmockExec) Returns(result driver.Result) ExpectedExec { e.exec = e.exec.WillReturnResult(result) From da8c7c18020d8aef8a59913403dbfdf56a43651b Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Tue, 21 Nov 2017 17:10:10 +0800 Subject: [PATCH 24/39] Add noop db driver --- expecter.go | 112 +++++++++++++++------------ expecter_noop.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++ expecter_test.go | 37 +++++++-- 3 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 expecter_noop.go diff --git a/expecter.go b/expecter.go index 1dab967c..dadbe8fc 100644 --- a/expecter.go +++ b/expecter.go @@ -1,14 +1,12 @@ package gorm import ( - "database/sql" - "errors" "regexp" ) // Recorder satisfies the logger interface type Recorder struct { - stmt string + stmts []Stmt } // Stmt represents a sql statement. It can be an Exec or Query @@ -41,11 +39,29 @@ func getStmtFromLog(values ...interface{}) Stmt { // TODO: find a better way to extract SQL from log messages func (r *Recorder) Print(args ...interface{}) { statement := getStmtFromLog(args...) + if statement.sql != "" { - r.stmt = statement.sql + r.stmts = append(r.stmts, statement) } } +// GetFirst returns the first recorded sql statement logged. If there are no +// statements, false is returned +func (r *Recorder) GetFirst() (Stmt, bool) { + var stmt Stmt + if len(r.stmts) > 0 { + stmt = r.stmts[0] + return stmt, true + } + + return stmt, false +} + +// IsEmpty returns true if the statements slice is empty +func (r *Recorder) IsEmpty() bool { + return len(r.stmts) == 0 +} + // AdapterFactory is a generic interface for arbitrary adapters that satisfy // the interface. variadic args are passed to gorm.Open. type AdapterFactory func(dialect string, args ...interface{}) (*DB, Adapter, error) @@ -54,52 +70,10 @@ type AdapterFactory func(dialect string, args ...interface{}) (*DB, Adapter, err type Expecter struct { // globally scoped expecter adapter Adapter - noop SQLCommon gorm *DB recorder *Recorder } -// DefaultNoopDB is a noop db used to get generated sql from gorm.DB -type DefaultNoopDB struct{} - -// NoopResult is a noop struct that satisfies sql.Result -type NoopResult struct{} - -// LastInsertId is a noop method for satisfying drive.Result -func (r NoopResult) LastInsertId() (int64, error) { - return 1, nil -} - -// RowsAffected is a noop method for satisfying drive.Result -func (r NoopResult) RowsAffected() (int64, error) { - return 1, nil -} - -// NewNoopDB initialises a new DefaultNoopDB -func NewNoopDB() SQLCommon { - return &DefaultNoopDB{} -} - -// Exec simulates a sql.DB.Exec -func (r *DefaultNoopDB) Exec(query string, args ...interface{}) (sql.Result, error) { - return NoopResult{}, nil -} - -// Prepare simulates a sql.DB.Prepare -func (r *DefaultNoopDB) Prepare(query string) (*sql.Stmt, error) { - return &sql.Stmt{}, nil -} - -// Query simulates a sql.DB.Query -func (r *DefaultNoopDB) Query(query string, args ...interface{}) (*sql.Rows, error) { - return nil, errors.New("noop") -} - -// QueryRow simulates a sql.DB.QueryRow -func (r *DefaultNoopDB) QueryRow(query string, args ...interface{}) *sql.Row { - return &sql.Row{} -} - // NewDefaultExpecter returns a Expecter powered by go-sqlmock func NewDefaultExpecter() (*DB, *Expecter, error) { gormDb, adapter, err := NewSqlmockAdapter("sqlmock", "mock_gorm_dsn") @@ -109,7 +83,7 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { } recorder := &Recorder{} - noop := &DefaultNoopDB{} + noop, _ := NewNoopDB() gorm := &DB{ db: noop, logger: recorder, @@ -121,7 +95,7 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm.parent = gorm - return gormDb, &Expecter{adapter: adapter, noop: noop, gorm: gorm, recorder: recorder}, nil + return gormDb, &Expecter{adapter: adapter, gorm: gorm, recorder: recorder}, nil } // NewExpecter returns an Expecter for arbitrary adapters @@ -144,12 +118,50 @@ func (h *Expecter) AssertExpectations() error { // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { + var q ExpectedQuery h.gorm.First(out, where...) - return h.adapter.ExpectQuery(regexp.QuoteMeta(h.recorder.stmt)) + + if empty := h.recorder.IsEmpty(); empty { + panic("No recorded statements") + } + + for _, stmt := range h.recorder.stmts { + q = h.adapter.ExpectQuery(regexp.QuoteMeta(stmt.sql)) + } + + return q } // Find triggers a Query func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { + var q ExpectedQuery h.gorm.Find(out, where...) - return h.adapter.ExpectQuery(regexp.QuoteMeta(h.recorder.stmt)) + + if empty := h.recorder.IsEmpty(); empty { + panic("No recorded statements") + } + + for _, stmt := range h.recorder.stmts { + q = h.adapter.ExpectQuery(regexp.QuoteMeta(stmt.sql)) + } + + return q +} + +// Preload clones the expecter and sets a preload condition on gorm.DB +func (h *Expecter) Preload(column string, conditions ...interface{}) *Expecter { + clone := h.clone() + clone.gorm = clone.gorm.Preload(column, conditions...) + + return clone +} + +/* PRIVATE METHODS */ + +func (h *Expecter) clone() *Expecter { + return &Expecter{ + adapter: h.adapter, + gorm: h.gorm, + recorder: h.recorder, + } } diff --git a/expecter_noop.go b/expecter_noop.go new file mode 100644 index 00000000..0ea9732d --- /dev/null +++ b/expecter_noop.go @@ -0,0 +1,193 @@ +package gorm + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "io" + "sync" + + "github.com/davecgh/go-spew/spew" +) + +var pool *NoopDriver + +func init() { + pool = &NoopDriver{ + conns: make(map[string]*NoopConnection), + } + + sql.Register("noop", pool) +} + +// NoopDriver implements sql/driver.Driver +type NoopDriver struct { + sync.Mutex + counter int + conns map[string]*NoopConnection +} + +// Open implements sql/driver.Driver +func (d *NoopDriver) Open(dsn string) (driver.Conn, error) { + d.Lock() + defer d.Unlock() + + c, ok := d.conns[dsn] + + if !ok { + return c, fmt.Errorf("No connection available") + } + + c.opened++ + return c, nil +} + +// NoopResult is a noop struct that satisfies sql.Result +type NoopResult struct{} + +// LastInsertId is a noop method for satisfying drive.Result +func (r NoopResult) LastInsertId() (int64, error) { + return 1, nil +} + +// RowsAffected is a noop method for satisfying drive.Result +func (r NoopResult) RowsAffected() (int64, error) { + return 1, nil +} + +// NoopRows implements driver.Rows +type NoopRows struct { + pos int +} + +// Columns implements driver.Rows +func (r *NoopRows) Columns() []string { + return []string{"foo", "bar", "baz", "lol", "kek", "zzz"} +} + +// Close implements driver.Rows +func (r *NoopRows) Close() error { + return nil +} + +// Next implements driver.Rows and alwys returns only one row +func (r *NoopRows) Next(dest []driver.Value) error { + if r.pos == 1 { + return io.EOF + } + cols := []string{"foo", "bar", "baz", "lol", "kek", "zzz"} + + for i, col := range cols { + dest[i] = col + } + + r.pos++ + + return nil +} + +// NoopStmt implements driver.Stmt +type NoopStmt struct{} + +// Close implements driver.Stmt +func (s *NoopStmt) Close() error { + return nil +} + +// NumInput implements driver.Stmt +func (s *NoopStmt) NumInput() int { + return 1 +} + +// Exec implements driver.Stmt +func (s *NoopStmt) Exec(args []driver.Value) (driver.Result, error) { + return &NoopResult{}, nil +} + +// Query implements driver.Stmt +func (s *NoopStmt) Query(args []driver.Value) (driver.Rows, error) { + return &NoopRows{}, nil +} + +// NewNoopDB initialises a new DefaultNoopDB +func NewNoopDB() (SQLCommon, error) { + pool.Lock() + dsn := fmt.Sprintf("noop_db_%d", pool.counter) + pool.counter++ + + noop := &NoopConnection{dsn: dsn, drv: pool} + pool.conns[dsn] = noop + pool.Unlock() + + db, err := noop.open() + + return db, err +} + +// NoopConnection implements sql/driver.Conn +// for our purposes, the noop connection never returns an error, as we only +// require it for generating queries. It is necessary because eagerloading +// will fail if any operation returns an error +type NoopConnection struct { + dsn string + drv *NoopDriver + opened int +} + +func (c *NoopConnection) open() (*sql.DB, error) { + db, err := sql.Open("noop", c.dsn) + + if err != nil { + return db, err + } + + fmt.Println(db.Ping()) + + return db, db.Ping() +} + +// Close implements sql/driver.Conn +func (c *NoopConnection) Close() error { + c.drv.Lock() + defer c.drv.Unlock() + + c.opened-- + if c.opened == 0 { + delete(c.drv.conns, c.dsn) + } + + return nil +} + +// Begin implements sql/driver.Conn +func (c *NoopConnection) Begin() (driver.Tx, error) { + fmt.Println("Called Begin()") + return c, nil +} + +// Exec implements sql/driver.Conn +func (c *NoopConnection) Exec(query string, args []driver.Value) (driver.Result, error) { + return NoopResult{}, nil +} + +// Prepare implements sql/driver.Conn +func (c *NoopConnection) Prepare(query string) (driver.Stmt, error) { + spew.Dump(query) + return &NoopStmt{}, nil +} + +// Query implements sql/driver.Conn +func (c *NoopConnection) Query(query string, args []driver.Value) (driver.Rows, error) { + spew.Dump(args) + return &NoopRows{}, nil +} + +// Commit implements sql/driver.Conn +func (c *NoopConnection) Commit() error { + return nil +} + +// Rollback implements sql/driver.Conn +func (c *NoopConnection) Rollback() error { + return nil +} diff --git a/expecter_test.go b/expecter_test.go index ba8cb8b7..aab5b22d 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -1,6 +1,7 @@ package gorm_test import ( + "fmt" "reflect" "testing" @@ -20,9 +21,7 @@ func TestNewDefaultExpecter(t *testing.T) { func TestNewCustomExpecter(t *testing.T) { db, _, err := gorm.NewExpecter(gorm.NewSqlmockAdapter, "sqlmock", "mock_gorm_dsn") - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -31,16 +30,14 @@ func TestNewCustomExpecter(t *testing.T) { func TestQuery(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() if err != nil { t.Fatal(err) } + fmt.Println("Got here") expect.First(&User{}) - db.LogMode(true).First(&User{}) + db.First(&User{}) if err := expect.AssertExpectations(); err != nil { t.Error(err) @@ -121,3 +118,29 @@ func TestFindSlice(t *testing.T) { t.Error("Expected equal slices") } } + +func TestMockPreload(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := User{Id: 1} + outEmails := []Email{Email{Id: 1, UserId: 1}, Email{Id: 2, UserId: 1}} + out := User{Id: 1, Emails: outEmails} + + expect.Preload("Emails").Find(&in).Returns(out) + db.Preload("Emails").Find(&in) + + // if err := expect.AssertExpectations(); err != nil { + // t.Error(err) + // } + + if !reflect.DeepEqual(in, out) { + t.Error("In and out are not equal") + } +} From b06542dc777fdbd3a4d1b994bdbb5e3d3e4b8ff6 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Tue, 21 Nov 2017 19:02:54 +0800 Subject: [PATCH 25/39] Support preloading --- expecter.go | 12 ++++++--- expecter_adapter.go | 14 ++++++---- expecter_result.go | 64 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/expecter.go b/expecter.go index dadbe8fc..5da09d09 100644 --- a/expecter.go +++ b/expecter.go @@ -2,6 +2,8 @@ package gorm import ( "regexp" + + "github.com/davecgh/go-spew/spew" ) // Recorder satisfies the logger interface @@ -38,6 +40,7 @@ func getStmtFromLog(values ...interface{}) Stmt { // Print just sets the last recorded SQL statement // TODO: find a better way to extract SQL from log messages func (r *Recorder) Print(args ...interface{}) { + spew.Dump(args...) statement := getStmtFromLog(args...) if statement.sql != "" { @@ -72,6 +75,7 @@ type Expecter struct { adapter Adapter gorm *DB recorder *Recorder + preload []string // records fields to be preloaded } // NewDefaultExpecter returns a Expecter powered by go-sqlmock @@ -134,7 +138,9 @@ func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { // Find triggers a Query func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { - var q ExpectedQuery + var ( + stmts []string + ) h.gorm.Find(out, where...) if empty := h.recorder.IsEmpty(); empty { @@ -142,10 +148,10 @@ func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { } for _, stmt := range h.recorder.stmts { - q = h.adapter.ExpectQuery(regexp.QuoteMeta(stmt.sql)) + stmts = append(stmts, regexp.QuoteMeta(stmt.sql)) } - return q + return h.adapter.ExpectQuery(stmts...) } // Preload clones the expecter and sets a preload condition on gorm.DB diff --git a/expecter_adapter.go b/expecter_adapter.go index 7e3d3da9..5ccc7271 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -24,7 +24,7 @@ func init() { // Adapter provides an abstract interface over concrete mock database // implementations (e.g. go-sqlmock or go-testdb) type Adapter interface { - ExpectQuery(stmt string) ExpectedQuery + ExpectQuery(stmts ...string) ExpectedQuery ExpectExec(stmt string) ExpectedExec AssertExpectations() error } @@ -49,11 +49,15 @@ func NewSqlmockAdapter(dialect string, args ...interface{}) (*DB, Adapter, error } // ExpectQuery wraps the underlying mock method for setting a query -// expectation -func (a *SqlmockAdapter) ExpectQuery(stmt string) ExpectedQuery { - q := a.mocker.ExpectQuery(stmt) +// expectation. It accepts multiple statements in the event of preloading +func (a *SqlmockAdapter) ExpectQuery(stmts ...string) ExpectedQuery { + var queries []*sqlmock.ExpectedQuery - return &SqlmockQuery{query: q} + for _, stmt := range stmts { + queries = append(queries, a.mocker.ExpectQuery(stmt)) + } + + return &SqlmockQuery{queries: queries} } // ExpectExec wraps the underlying mock method for setting a exec diff --git a/expecter_result.go b/expecter_result.go index 0f2ecc6e..d659f97e 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -24,7 +24,7 @@ type ExpectedExec interface { // SqlmockQuery implements Query for go-sqlmock type SqlmockQuery struct { - query *sqlmock.ExpectedQuery + queries []*sqlmock.ExpectedQuery } func getRowForFields(fields []*Field) []driver.Value { @@ -49,6 +49,8 @@ func getRowForFields(fields []*Field) []driver.Value { if driver.IsValue(concreteVal) { values = append(values, concreteVal) + } else if value.Kind() == reflect.Int || value.Kind() == reflect.Int8 || value.Kind() == reflect.Int16 || value.Kind() == reflect.Int64 { + values = append(values, value.Int()) } else if valuer, ok := concreteVal.(driver.Valuer); ok { if convertedValue, err := valuer.Value(); err == nil { values = append(values, convertedValue) @@ -60,13 +62,28 @@ func getRowForFields(fields []*Field) []driver.Value { return values } -func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { - var columns []string +func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { + var ( + columns []string + relations = make(map[string]*Relationship) + rowsSet []*sqlmock.Rows + ) for _, field := range (&Scope{}).New(out).GetModelStruct().StructFields { + // we get the primary model's columns here if field.IsNormal { columns = append(columns, field.DBName) } + + // check relations + if !field.IsNormal { + relationVal := reflect.ValueOf(field.Relationship) + isNil := relationVal.IsNil() + + if !isNil { + relations[field.Name] = field.Relationship + } + } } rows := sqlmock.NewRows(columns) @@ -83,16 +100,50 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { scope := &Scope{Value: outElem} row := getRowForFields(scope.Fields()) rows = rows.AddRow(row...) + rowsSet = append(rowsSet, rows) } } else if outVal.Kind() == reflect.Struct { scope := &Scope{Value: out} row := getRowForFields(scope.Fields()) rows = rows.AddRow(row...) + rowsSet = append(rowsSet, rows) + + for name, relation := range relations { + switch relation.Kind { + case "has_many": + rVal := outVal.FieldByName(name) + rType := rVal.Type().Elem() + rScope := &Scope{Value: reflect.New(rType).Interface()} + rColumns := []string{} + + for _, field := range rScope.GetModelStruct().StructFields { + rColumns = append(rColumns, field.DBName) + } + + hasReturnRows := rVal.Len() > 0 + + // in this case we definitely have a slice + if hasReturnRows { + rRows := sqlmock.NewRows(rColumns) + + for i := 0; i < rVal.Len(); i++ { + scope := &Scope{Value: rVal.Index(i).Interface()} + row := getRowForFields(scope.Fields()) + rRows = rRows.AddRow(row...) + rowsSet = append(rowsSet, rRows) + } + } + case "has_one": + case "many2many": + default: + continue + } + } } else { panic(fmt.Errorf("Can only get rows for slice or struct")) } - return rows + return rowsSet } // Returns accepts an out type which should either be a struct or slice. Under @@ -100,7 +151,10 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) *sqlmock.Rows { // the underlying mock db func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { rows := q.getRowsForOutType(out) - q.query = q.query.WillReturnRows(rows) + + for i, query := range q.queries { + query.WillReturnRows(rows[i]) + } return q } From 7aa08b901450ace44b12f252c2ae945119ae5da9 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Tue, 21 Nov 2017 19:32:17 +0800 Subject: [PATCH 26/39] Refactor code for extracting has_many relations --- expecter.go | 3 --- expecter_noop.go | 7 ------ expecter_result.go | 63 ++++++++++++++++++++++++++-------------------- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/expecter.go b/expecter.go index 5da09d09..41d23ce3 100644 --- a/expecter.go +++ b/expecter.go @@ -2,8 +2,6 @@ package gorm import ( "regexp" - - "github.com/davecgh/go-spew/spew" ) // Recorder satisfies the logger interface @@ -40,7 +38,6 @@ func getStmtFromLog(values ...interface{}) Stmt { // Print just sets the last recorded SQL statement // TODO: find a better way to extract SQL from log messages func (r *Recorder) Print(args ...interface{}) { - spew.Dump(args...) statement := getStmtFromLog(args...) if statement.sql != "" { diff --git a/expecter_noop.go b/expecter_noop.go index 0ea9732d..fc9c9d89 100644 --- a/expecter_noop.go +++ b/expecter_noop.go @@ -6,8 +6,6 @@ import ( "fmt" "io" "sync" - - "github.com/davecgh/go-spew/spew" ) var pool *NoopDriver @@ -141,8 +139,6 @@ func (c *NoopConnection) open() (*sql.DB, error) { return db, err } - fmt.Println(db.Ping()) - return db, db.Ping() } @@ -161,7 +157,6 @@ func (c *NoopConnection) Close() error { // Begin implements sql/driver.Conn func (c *NoopConnection) Begin() (driver.Tx, error) { - fmt.Println("Called Begin()") return c, nil } @@ -172,13 +167,11 @@ func (c *NoopConnection) Exec(query string, args []driver.Value) (driver.Result, // Prepare implements sql/driver.Conn func (c *NoopConnection) Prepare(query string) (driver.Stmt, error) { - spew.Dump(query) return &NoopStmt{}, nil } // Query implements sql/driver.Conn func (c *NoopConnection) Query(query string, args []driver.Value) (driver.Rows, error) { - spew.Dump(args) return &NoopRows{}, nil } diff --git a/expecter_result.go b/expecter_result.go index d659f97e..e96f7c8c 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -62,6 +62,38 @@ func getRowForFields(fields []*Field) []driver.Value { return values } +func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationship) (*sqlmock.Rows, bool) { + var ( + rows *sqlmock.Rows + columns []string + ) + + switch relation.Kind { + case "has_many": + elem := rVal.Type().Elem() + scope := &Scope{Value: reflect.New(elem).Interface()} + + for _, field := range scope.GetModelStruct().StructFields { + columns = append(columns, field.DBName) + } + + rows = sqlmock.NewRows(columns) + + // in this case we definitely have a slice + if rVal.Len() > 0 { + for i := 0; i < rVal.Len(); i++ { + scope := &Scope{Value: rVal.Index(i).Interface()} + row := getRowForFields(scope.Fields()) + rows = rows.AddRow(row...) + } + } + + return rows, true + default: + return nil, false + } +} + func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { var ( columns []string @@ -109,34 +141,11 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { rowsSet = append(rowsSet, rows) for name, relation := range relations { - switch relation.Kind { - case "has_many": - rVal := outVal.FieldByName(name) - rType := rVal.Type().Elem() - rScope := &Scope{Value: reflect.New(rType).Interface()} - rColumns := []string{} + rVal := outVal.FieldByName(name) + relationRows, hasRows := getRelationRows(rVal, name, relation) - for _, field := range rScope.GetModelStruct().StructFields { - rColumns = append(rColumns, field.DBName) - } - - hasReturnRows := rVal.Len() > 0 - - // in this case we definitely have a slice - if hasReturnRows { - rRows := sqlmock.NewRows(rColumns) - - for i := 0; i < rVal.Len(); i++ { - scope := &Scope{Value: rVal.Index(i).Interface()} - row := getRowForFields(scope.Fields()) - rRows = rRows.AddRow(row...) - rowsSet = append(rowsSet, rRows) - } - } - case "has_one": - case "many2many": - default: - continue + if hasRows { + rowsSet = append(rowsSet, relationRows) } } } else { From d28ab5ae897be365be0b2475845b1a0ce72cb0ad Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Wed, 22 Nov 2017 09:47:47 +0800 Subject: [PATCH 27/39] Add back AssertExpectations --- expecter_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/expecter_test.go b/expecter_test.go index aab5b22d..9eedd49c 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -136,9 +136,9 @@ func TestMockPreload(t *testing.T) { expect.Preload("Emails").Find(&in).Returns(out) db.Preload("Emails").Find(&in) - // if err := expect.AssertExpectations(); err != nil { - // t.Error(err) - // } + if err := expect.AssertExpectations(); err != nil { + t.Error(err) + } if !reflect.DeepEqual(in, out) { t.Error("In and out are not equal") From 8ae27d7ec78011f7e54a962ef46ba8ee615df229 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Wed, 22 Nov 2017 15:23:02 +0800 Subject: [PATCH 28/39] Add partial support for many_to_many --- expecter_result.go | 10 ++++++---- expecter_test.go | 28 +++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/expecter_result.go b/expecter_result.go index e96f7c8c..59c2379b 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -49,8 +49,8 @@ func getRowForFields(fields []*Field) []driver.Value { if driver.IsValue(concreteVal) { values = append(values, concreteVal) - } else if value.Kind() == reflect.Int || value.Kind() == reflect.Int8 || value.Kind() == reflect.Int16 || value.Kind() == reflect.Int64 { - values = append(values, value.Int()) + } else if num, err := driver.DefaultParameterConverter.ConvertValue(concreteVal); err == nil { + values = append(values, num) } else if valuer, ok := concreteVal.(driver.Valuer); ok { if convertedValue, err := valuer.Value(); err == nil { values = append(values, convertedValue) @@ -69,12 +69,14 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi ) switch relation.Kind { - case "has_many": + case "has_many", "many_to_many": elem := rVal.Type().Elem() scope := &Scope{Value: reflect.New(elem).Interface()} for _, field := range scope.GetModelStruct().StructFields { - columns = append(columns, field.DBName) + if field.IsNormal { + columns = append(columns, field.DBName) + } } rows = sqlmock.NewRows(columns) diff --git a/expecter_test.go b/expecter_test.go index 9eedd49c..6b84f198 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -119,7 +119,7 @@ func TestFindSlice(t *testing.T) { } } -func TestMockPreload(t *testing.T) { +func TestMockPreloadHasMany(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() defer func() { db.Close() @@ -144,3 +144,29 @@ func TestMockPreload(t *testing.T) { t.Error("In and out are not equal") } } + +func TestMockPreloadMany2Many(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := User{Id: 1} + languages := []Language{Language{Name: "ZH"}, Language{Name: "EN"}} + out := User{Id: 1, Languages: languages} + + expect.Preload("Languages").Find(&in).Returns(out) + db.Preload("Languages").Find(&in) + + if err := expect.AssertExpectations(); err != nil { + t.Error(err) + } + + // if !reflect.DeepEqual(in, out) { + // t.Error("In and out are not equal") + // } +} From a0b70669e7b148b58faf65de667a8ca8bf738e45 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Wed, 22 Nov 2017 15:44:06 +0800 Subject: [PATCH 29/39] Support mock has one --- expecter_result.go | 20 ++++++++++++++++++++ expecter_test.go | 31 ++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/expecter_result.go b/expecter_result.go index 59c2379b..b983672d 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -69,7 +70,25 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi ) switch relation.Kind { + case "has_one": + // just a plain struct + scope := &Scope{Value: rVal.Interface()} + + for _, field := range scope.GetModelStruct().StructFields { + if field.IsNormal { + columns = append(columns, field.DBName) + } + } + + rows = sqlmock.NewRows(columns) + + // we don't have a slice + row := getRowForFields(scope.Fields()) + rows = rows.AddRow(row...) + + return rows, true case "has_many", "many_to_many": + // in this case, we're guarnateed to have a slice elem := rVal.Type().Elem() scope := &Scope{Value: reflect.New(elem).Interface()} @@ -165,6 +184,7 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { for i, query := range q.queries { query.WillReturnRows(rows[i]) + spew.Dump(query) } return q diff --git a/expecter_test.go b/expecter_test.go index 6b84f198..fbdbbb2d 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -145,6 +145,31 @@ func TestMockPreloadHasMany(t *testing.T) { } } +func TestMockPreloadHasOne(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + in := User{Id: 1} + out := User{Id: 1, CreditCard: CreditCard{Number: "12345678"}} + + expect.Preload("CreditCard").Find(&in).Returns(out) + db.Preload("CreditCard").Find(&in) + + if err := expect.AssertExpectations(); err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(in, out) { + t.Error("In and out are not equal") + } +} + func TestMockPreloadMany2Many(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() defer func() { @@ -166,7 +191,7 @@ func TestMockPreloadMany2Many(t *testing.T) { t.Error(err) } - // if !reflect.DeepEqual(in, out) { - // t.Error("In and out are not equal") - // } + // if !reflect.DeepEqual(in, out) { + // t.Error("In and out are not equal") + // } } From 8cf623a01f19f196042e4fc862bcc27e5503d5b5 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Wed, 22 Nov 2017 15:48:15 +0800 Subject: [PATCH 30/39] Minor clean up --- expecter_result.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/expecter_result.go b/expecter_result.go index b983672d..1e5e02b2 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" - "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -71,7 +70,6 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi switch relation.Kind { case "has_one": - // just a plain struct scope := &Scope{Value: rVal.Interface()} for _, field := range scope.GetModelStruct().StructFields { @@ -88,7 +86,6 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi return rows, true case "has_many", "many_to_many": - // in this case, we're guarnateed to have a slice elem := rVal.Type().Elem() scope := &Scope{Value: reflect.New(elem).Interface()} @@ -184,7 +181,6 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { for i, query := range q.queries { query.WillReturnRows(rows[i]) - spew.Dump(query) } return q From c2a28c63c3145c9d0a74a2e7b930c5de0839ca3e Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Wed, 22 Nov 2017 20:53:49 +0800 Subject: [PATCH 31/39] Fix mock preload many2many generating empty relation rows --- callback_query_preload.go | 11 +++++++++++ expecter_result.go | 18 +++++++++++++++++- expecter_test.go | 12 ++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/callback_query_preload.go b/callback_query_preload.go index 21ab22ce..d136aee4 100644 --- a/callback_query_preload.go +++ b/callback_query_preload.go @@ -60,6 +60,7 @@ func preloadCallback(scope *Scope) { currentScope.handleBelongsToPreload(field, currentPreloadConditions) case "many_to_many": currentScope.handleManyToManyPreload(field, currentPreloadConditions) + default: scope.Err(errors.New("unsupported relation")) } @@ -264,6 +265,8 @@ func (scope *Scope) handleBelongsToPreload(field *Field, conditions []interface{ // handleManyToManyPreload used to preload many to many associations func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface{}) { + // spew.Println("___ENTERING HANDLE MANY TO MANY___\r\n") + // spew.Printf("___POPULATING %s___:\r\n%s\r\n", field.Name, spew.Sdump(field)) var ( relation = field.Relationship joinTableHandler = relation.JoinTableHandler @@ -303,6 +306,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface } rows, err := preloadDB.Rows() + // spew.Printf("___RETURNED ROWS___: \r\n%s\r\n", spew.Sdump(rows)) if scope.Err(err) != nil { return @@ -312,6 +316,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface columns, _ := rows.Columns() for rows.Next() { var ( + // This is a Language zero value struct elem = reflect.New(fieldType).Elem() fields = scope.New(elem.Addr().Interface()).Fields() ) @@ -323,6 +328,7 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface } scope.scan(rows, columns, append(fields, joinTableFields...)) + // spew.Printf("___FIELDS___: \r\n%s\r\n", spew.Sdump(fields)) var foreignKeys = make([]interface{}, len(sourceKeys)) // generate hashed forkey keys in join table @@ -351,12 +357,14 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface foreignFieldNames = []string{} ) + // spew.Printf("Foreign fields: %s", spew.Sdump(relation.ForeignFieldNames)) for _, dbName := range relation.ForeignFieldNames { if field, ok := scope.FieldByName(dbName); ok { foreignFieldNames = append(foreignFieldNames, field.Name) } } + // spew.Printf("Scope value: %s", spew.Sdump(indirectScopeValue)) if indirectScopeValue.Kind() == reflect.Slice { for j := 0; j < indirectScopeValue.Len(); j++ { object := indirect(indirectScopeValue.Index(j)) @@ -367,6 +375,9 @@ func (scope *Scope) handleManyToManyPreload(field *Field, conditions []interface key := toString(getValueFromFields(indirectScopeValue, foreignFieldNames)) fieldsSourceMap[key] = append(fieldsSourceMap[key], indirectScopeValue.FieldByName(field.Name)) } + + // spew.Printf("Field source map: %s", spew.Sdump(fieldsSourceMap)) + // spew.Printf("Link hash: %s", spew.Sdump(linkHash)) for source, link := range linkHash { for i, field := range fieldsSourceMap[source] { //If not 0 this means Value is a pointer and we already added preloaded models to it diff --git a/expecter_result.go b/expecter_result.go index 1e5e02b2..f5c35c93 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -46,6 +47,7 @@ func getRowForFields(fields []*Field) []driver.Value { } concreteVal := value.Interface() + // spew.Printf("%v: %v\r\n", field.Name, concreteVal) if driver.IsValue(concreteVal) { values = append(values, concreteVal) @@ -68,6 +70,12 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi columns []string ) + // we need to check for zero values + if reflect.DeepEqual(rVal.Interface(), reflect.New(rVal.Type()).Elem().Interface()) { + spew.Printf("FOUND EMPTY INTERFACE FOR %s\r\n", fieldName) + return nil, false + } + switch relation.Kind { case "has_one": scope := &Scope{Value: rVal.Interface()} @@ -94,6 +102,9 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi columns = append(columns, field.DBName) } } + spew.Printf("___GENERATING ROWS FOR %s___\r\n", fieldName) + spew.Printf("___COLUMNS___:\r\n%s\r\n", spew.Sdump(columns)) + columns = append(columns, "user_id", "language_id") rows = sqlmock.NewRows(columns) @@ -102,11 +113,14 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi for i := 0; i < rVal.Len(); i++ { scope := &Scope{Value: rVal.Index(i).Interface()} row := getRowForFields(scope.Fields()) + row = append(row, 1, 1) rows = rows.AddRow(row...) } + + return rows, true } - return rows, true + return nil, false default: return nil, false } @@ -163,6 +177,7 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { relationRows, hasRows := getRelationRows(rVal, name, relation) if hasRows { + spew.Printf("___GENERATED ROWS FOR %s___\r\n: %s\r\n", name, spew.Sdump(relationRows)) rowsSet = append(rowsSet, relationRows) } } @@ -181,6 +196,7 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { for i, query := range q.queries { query.WillReturnRows(rows[i]) + spew.Printf("___SET RETURN ROW___: %s", spew.Sdump(rows[i])) } return q diff --git a/expecter_test.go b/expecter_test.go index fbdbbb2d..64c70e90 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/davecgh/go-spew/spew" "github.com/jinzhu/gorm" ) @@ -181,7 +182,7 @@ func TestMockPreloadMany2Many(t *testing.T) { } in := User{Id: 1} - languages := []Language{Language{Name: "ZH"}, Language{Name: "EN"}} + languages := []Language{Language{Name: "ZH"}} out := User{Id: 1, Languages: languages} expect.Preload("Languages").Find(&in).Returns(out) @@ -191,7 +192,10 @@ func TestMockPreloadMany2Many(t *testing.T) { t.Error(err) } - // if !reflect.DeepEqual(in, out) { - // t.Error("In and out are not equal") - // } + spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) + spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) + + if !reflect.DeepEqual(in, out) { + t.Error("In and out are not equal") + } } From 1a384b3c0c766342006cda84e653e14c78db689c Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 23 Nov 2017 17:25:40 +0800 Subject: [PATCH 32/39] Use callbacks to record sql instead --- expecter.go | 97 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/expecter.go b/expecter.go index 41d23ce3..947d9afb 100644 --- a/expecter.go +++ b/expecter.go @@ -1,19 +1,70 @@ package gorm import ( - "regexp" + "fmt" ) // Recorder satisfies the logger interface type Recorder struct { - stmts []Stmt + stmts []Stmt + preload []searchPreload // store it on Recorder } -// Stmt represents a sql statement. It can be an Exec or Query +// Stmt represents a sql statement. It can be an Exec, Query, or QueryRow type Stmt struct { - stmtType string - sql string - args []interface{} + kind string // can be Query, Exec, QueryRow + preload string // contains schema if it is a preload query + sql string + args []interface{} +} + +func recordQueryCallback(scope *Scope) { + r, ok := scope.Get("gorm:recorder") + + if !ok { + panic(fmt.Errorf("Expected a recorder to be set, but got none")) + } + + stmt := Stmt{ + kind: "query", + sql: scope.SQL, + args: scope.SQLVars, + } + + recorder := r.(*Recorder) + + if len(recorder.preload) > 0 { + // this will cause the scope.SQL to mutate to the preload query + scope.prepareQuerySQL() + stmt.preload = recorder.preload[0].schema + + // spew.Printf("_____PRELOADING_____\r\n%s\r\n", stmt.preload) + // spew.Printf("_____SQL_____\r\n%s\r\n", scope.SQL) + + // we just want to pop the first element off + recorder.preload = recorder.preload[1:] + } + + recorder.Record(stmt) +} + +func recordPreloadCallback(scope *Scope) { + // this callback runs _before_ gorm:preload + // it should record the next thing to be preloaded + recorder, ok := scope.Get("gorm:recorder") + + if !ok { + panic(fmt.Errorf("Expected a recorder to be set, but got none")) + } + if len(scope.Search.preload) > 0 { + // spew.Printf("callback:preload\r\n%s\r\n", spew.Sdump(scope.Search.preload)) + recorder.(*Recorder).preload = scope.Search.preload + } +} + +// Record records a Stmt for use when SQL is finally executed +func (r *Recorder) Record(stmt Stmt) { + r.stmts = append(r.stmts, stmt) } func getStmtFromLog(values ...interface{}) Stmt { @@ -72,7 +123,6 @@ type Expecter struct { adapter Adapter gorm *DB recorder *Recorder - preload []string // records fields to be preloaded } // NewDefaultExpecter returns a Expecter powered by go-sqlmock @@ -87,7 +137,7 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { noop, _ := NewNoopDB() gorm := &DB{ db: noop, - logger: recorder, + logger: defaultLogger, logMode: 2, values: map[string]interface{}{}, callbacks: DefaultCallback, @@ -95,6 +145,11 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { } gorm.parent = gorm + gorm = gorm.Set("gorm:recorder", recorder) + gorm = gorm.Set("gorm:preload_idx", 0) + gorm.Callback().Query().Before("gorm:preload").Register("gorm:record_preload", recordPreloadCallback) + gorm.Callback().Query().After("gorm:query").Register("gorm:record_query", recordQueryCallback) + gorm.Callback().RowQuery().Before("gorm:row_query").Register("gorm:record_query", recordQueryCallback) return gormDb, &Expecter{adapter: adapter, gorm: gorm, recorder: recorder}, nil } @@ -119,36 +174,14 @@ func (h *Expecter) AssertExpectations() error { // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { - var q ExpectedQuery h.gorm.First(out, where...) - - if empty := h.recorder.IsEmpty(); empty { - panic("No recorded statements") - } - - for _, stmt := range h.recorder.stmts { - q = h.adapter.ExpectQuery(regexp.QuoteMeta(stmt.sql)) - } - - return q + return h.adapter.ExpectQuery(h.recorder.stmts...) } // Find triggers a Query func (h *Expecter) Find(out interface{}, where ...interface{}) ExpectedQuery { - var ( - stmts []string - ) h.gorm.Find(out, where...) - - if empty := h.recorder.IsEmpty(); empty { - panic("No recorded statements") - } - - for _, stmt := range h.recorder.stmts { - stmts = append(stmts, regexp.QuoteMeta(stmt.sql)) - } - - return h.adapter.ExpectQuery(stmts...) + return h.adapter.ExpectQuery(h.recorder.stmts...) } // Preload clones the expecter and sets a preload condition on gorm.DB From e89019d1782806e94a4f705e48d2ae90f158252d Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 23 Nov 2017 17:26:24 +0800 Subject: [PATCH 33/39] Remove unnecessary Set --- expecter.go | 1 - 1 file changed, 1 deletion(-) diff --git a/expecter.go b/expecter.go index 947d9afb..aa12f30c 100644 --- a/expecter.go +++ b/expecter.go @@ -146,7 +146,6 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm.parent = gorm gorm = gorm.Set("gorm:recorder", recorder) - gorm = gorm.Set("gorm:preload_idx", 0) gorm.Callback().Query().Before("gorm:preload").Register("gorm:record_preload", recordPreloadCallback) gorm.Callback().Query().After("gorm:query").Register("gorm:record_query", recordQueryCallback) gorm.Callback().RowQuery().Before("gorm:row_query").Register("gorm:record_query", recordQueryCallback) From 505ecd17d3abc6b544f792855e12e614225a19ba Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 23 Nov 2017 17:27:59 +0800 Subject: [PATCH 34/39] Pass Stmt directly to SqlmockQuery --- expecter_adapter.go | 12 ++----- expecter_result.go | 78 ++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 52 deletions(-) diff --git a/expecter_adapter.go b/expecter_adapter.go index 5ccc7271..62929678 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -24,7 +24,7 @@ func init() { // Adapter provides an abstract interface over concrete mock database // implementations (e.g. go-sqlmock or go-testdb) type Adapter interface { - ExpectQuery(stmts ...string) ExpectedQuery + ExpectQuery(stmts ...Stmt) ExpectedQuery ExpectExec(stmt string) ExpectedExec AssertExpectations() error } @@ -50,14 +50,8 @@ func NewSqlmockAdapter(dialect string, args ...interface{}) (*DB, Adapter, error // ExpectQuery wraps the underlying mock method for setting a query // expectation. It accepts multiple statements in the event of preloading -func (a *SqlmockAdapter) ExpectQuery(stmts ...string) ExpectedQuery { - var queries []*sqlmock.ExpectedQuery - - for _, stmt := range stmts { - queries = append(queries, a.mocker.ExpectQuery(stmt)) - } - - return &SqlmockQuery{queries: queries} +func (a *SqlmockAdapter) ExpectQuery(queries ...Stmt) ExpectedQuery { + return &SqlmockQuery{mock: a.mocker, queries: queries} } // ExpectExec wraps the underlying mock method for setting a exec diff --git a/expecter_result.go b/expecter_result.go index f5c35c93..f8faab5b 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -4,8 +4,8 @@ import ( "database/sql/driver" "fmt" "reflect" + "regexp" - "github.com/davecgh/go-spew/spew" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -25,7 +25,8 @@ type ExpectedExec interface { // SqlmockQuery implements Query for go-sqlmock type SqlmockQuery struct { - queries []*sqlmock.ExpectedQuery + mock sqlmock.Sqlmock + queries []Stmt } func getRowForFields(fields []*Field) []driver.Value { @@ -64,7 +65,7 @@ func getRowForFields(fields []*Field) []driver.Value { return values } -func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationship) (*sqlmock.Rows, bool) { +func (q *SqlmockQuery) getRelationRows(rVal reflect.Value, fieldName string, relation *Relationship) (*sqlmock.Rows, bool) { var ( rows *sqlmock.Rows columns []string @@ -72,7 +73,7 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi // we need to check for zero values if reflect.DeepEqual(rVal.Interface(), reflect.New(rVal.Type()).Elem().Interface()) { - spew.Printf("FOUND EMPTY INTERFACE FOR %s\r\n", fieldName) + // spew.Printf("FOUND EMPTY INTERFACE FOR %s\r\n", fieldName) return nil, false } @@ -102,8 +103,8 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi columns = append(columns, field.DBName) } } - spew.Printf("___GENERATING ROWS FOR %s___\r\n", fieldName) - spew.Printf("___COLUMNS___:\r\n%s\r\n", spew.Sdump(columns)) + // spew.Printf("___GENERATING ROWS FOR %s___\r\n", fieldName) + // spew.Printf("___COLUMNS___:\r\n%s\r\n", spew.Sdump(columns)) columns = append(columns, "user_id", "language_id") rows = sqlmock.NewRows(columns) @@ -126,36 +127,21 @@ func getRelationRows(rVal reflect.Value, fieldName string, relation *Relationshi } } -func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { - var ( - columns []string - relations = make(map[string]*Relationship) - rowsSet []*sqlmock.Rows - ) - +func (q *SqlmockQuery) getDestRows(out interface{}) *sqlmock.Rows { + var columns []string for _, field := range (&Scope{}).New(out).GetModelStruct().StructFields { - // we get the primary model's columns here if field.IsNormal { columns = append(columns, field.DBName) } - - // check relations - if !field.IsNormal { - relationVal := reflect.ValueOf(field.Relationship) - isNil := relationVal.IsNil() - - if !isNil { - relations[field.Name] = field.Relationship - } - } } rows := sqlmock.NewRows(columns) - outVal := indirect(reflect.ValueOf(out)) + // SELECT multiple columns if outVal.Kind() == reflect.Slice { outSlice := []interface{}{} + for i := 0; i < outVal.Len(); i++ { outSlice = append(outSlice, outVal.Index(i).Interface()) } @@ -164,39 +150,45 @@ func (q *SqlmockQuery) getRowsForOutType(out interface{}) []*sqlmock.Rows { scope := &Scope{Value: outElem} row := getRowForFields(scope.Fields()) rows = rows.AddRow(row...) - rowsSet = append(rowsSet, rows) } - } else if outVal.Kind() == reflect.Struct { + } else if outVal.Kind() == reflect.Struct { // SELECT with LIMIT 1 scope := &Scope{Value: out} row := getRowForFields(scope.Fields()) rows = rows.AddRow(row...) - rowsSet = append(rowsSet, rows) - - for name, relation := range relations { - rVal := outVal.FieldByName(name) - relationRows, hasRows := getRelationRows(rVal, name, relation) - - if hasRows { - spew.Printf("___GENERATED ROWS FOR %s___\r\n: %s\r\n", name, spew.Sdump(relationRows)) - rowsSet = append(rowsSet, relationRows) - } - } } else { panic(fmt.Errorf("Can only get rows for slice or struct")) } - return rowsSet + return rows } // Returns accepts an out type which should either be a struct or slice. Under // the hood, it converts a gorm model struct to sql.Rows that can be passed to // the underlying mock db func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { - rows := q.getRowsForOutType(out) + scope := (&Scope{}).New(out) + outVal := indirect(reflect.ValueOf(out)) - for i, query := range q.queries { - query.WillReturnRows(rows[i]) - spew.Printf("___SET RETURN ROW___: %s", spew.Sdump(rows[i])) + // rows := q.getRowsForOutType(out) + destQuery := q.queries[0] + subQueries := q.queries[1:] + + // main query always at the head of the slice + q.mock.ExpectQuery(regexp.QuoteMeta(destQuery.sql)). + WillReturnRows(q.getDestRows(out)) + + // subqueries are preload + for _, subQuery := range subQueries { + if subQuery.preload != "" { + if field, ok := scope.FieldByName(subQuery.preload); ok { + expectation := q.mock.ExpectQuery(regexp.QuoteMeta(subQuery.sql)) + rows, hasRows := q.getRelationRows(outVal.FieldByName(subQuery.preload), subQuery.preload, field.Relationship) + + if hasRows { + expectation.WillReturnRows(rows) + } + } + } } return q From 5ef2153ab4a6dbb97466f29e7575e2a3c0e25098 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 23 Nov 2017 17:28:10 +0800 Subject: [PATCH 35/39] Support multiple preload --- expecter_test.go | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/expecter_test.go b/expecter_test.go index 64c70e90..315f20a5 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -5,7 +5,6 @@ import ( "reflect" "testing" - "github.com/davecgh/go-spew/spew" "github.com/jinzhu/gorm" ) @@ -192,8 +191,39 @@ func TestMockPreloadMany2Many(t *testing.T) { t.Error(err) } - spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) - spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) + // spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) + // spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) + + if !reflect.DeepEqual(in, out) { + t.Error("In and out are not equal") + } +} + +func TestMockPreloadMultiple(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + creditCard := CreditCard{Number: "12345678"} + languages := []Language{Language{Name: "ZH"}} + + in := User{Id: 1} + out := User{Id: 1, Languages: languages, CreditCard: creditCard} + + expect.Preload("Languages").Preload("CreditCard").Find(&in).Returns(out) + db.Preload("Languages").Preload("CreditCard").Find(&in) + + if err := expect.AssertExpectations(); err != nil { + t.Error(err) + } + + // spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) + // spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) if !reflect.DeepEqual(in, out) { t.Error("In and out are not equal") From 4128722761065e2b263d35006440c01355404a72 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Thu, 23 Nov 2017 18:39:06 +0800 Subject: [PATCH 36/39] Automatically insert join table values for many_to_many mock queries --- expecter_result.go | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/expecter_result.go b/expecter_result.go index f8faab5b..f68ec388 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -27,6 +27,7 @@ type ExpectedExec interface { type SqlmockQuery struct { mock sqlmock.Sqlmock queries []Stmt + scope *Scope } func getRowForFields(fields []*Field) []driver.Value { @@ -94,7 +95,7 @@ func (q *SqlmockQuery) getRelationRows(rVal reflect.Value, fieldName string, rel rows = rows.AddRow(row...) return rows, true - case "has_many", "many_to_many": + case "has_many": elem := rVal.Type().Elem() scope := &Scope{Value: reflect.New(elem).Interface()} @@ -103,9 +104,38 @@ func (q *SqlmockQuery) getRelationRows(rVal reflect.Value, fieldName string, rel columns = append(columns, field.DBName) } } - // spew.Printf("___GENERATING ROWS FOR %s___\r\n", fieldName) - // spew.Printf("___COLUMNS___:\r\n%s\r\n", spew.Sdump(columns)) - columns = append(columns, "user_id", "language_id") + + rows = sqlmock.NewRows(columns) + + if rVal.Len() > 0 { + for i := 0; i < rVal.Len(); i++ { + scope := &Scope{Value: rVal.Index(i).Interface()} + row := getRowForFields(scope.Fields()) + rows = rows.AddRow(row...) + } + + return rows, true + } + + return nil, false + case "many_to_many": + elem := rVal.Type().Elem() + scope := &Scope{Value: reflect.New(elem).Interface()} + joinTable := relation.JoinTableHandler.(*JoinTableHandler) + + for _, field := range scope.GetModelStruct().StructFields { + if field.IsNormal { + columns = append(columns, field.DBName) + } + } + + for _, key := range joinTable.Source.ForeignKeys { + columns = append(columns, key.DBName) + } + + for _, key := range joinTable.Destination.ForeignKeys { + columns = append(columns, key.DBName) + } rows = sqlmock.NewRows(columns) @@ -114,7 +144,15 @@ func (q *SqlmockQuery) getRelationRows(rVal reflect.Value, fieldName string, rel for i := 0; i < rVal.Len(); i++ { scope := &Scope{Value: rVal.Index(i).Interface()} row := getRowForFields(scope.Fields()) - row = append(row, 1, 1) + + // need to append the values for join table keys + sourcePk := q.scope.PrimaryKeyValue() + destModelType := joinTable.Destination.ModelType + destModelVal := reflect.New(destModelType).Interface() + destPkVal := (&Scope{Value: destModelVal}).PrimaryKeyValue() + + row = append(row, sourcePk, destPkVal) + rows = rows.AddRow(row...) } @@ -152,8 +190,7 @@ func (q *SqlmockQuery) getDestRows(out interface{}) *sqlmock.Rows { rows = rows.AddRow(row...) } } else if outVal.Kind() == reflect.Struct { // SELECT with LIMIT 1 - scope := &Scope{Value: out} - row := getRowForFields(scope.Fields()) + row := getRowForFields(q.scope.Fields()) rows = rows.AddRow(row...) } else { panic(fmt.Errorf("Can only get rows for slice or struct")) @@ -167,6 +204,7 @@ func (q *SqlmockQuery) getDestRows(out interface{}) *sqlmock.Rows { // the underlying mock db func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { scope := (&Scope{}).New(out) + q.scope = scope outVal := indirect(reflect.ValueOf(out)) // rows := q.getRowsForOutType(out) From 2368c373aec21c749037459caeb5607c7c897735 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 24 Nov 2017 15:32:43 +0800 Subject: [PATCH 37/39] Support basic mock Create use cases --- expecter.go | 62 +++++++++++++++++++++------------------------ expecter_adapter.go | 8 +++--- expecter_noop.go | 4 +-- expecter_result.go | 22 ++++++++++++---- expecter_test.go | 49 ++++++++++++++++++++++++++++++++--- 5 files changed, 97 insertions(+), 48 deletions(-) diff --git a/expecter.go b/expecter.go index aa12f30c..cd746436 100644 --- a/expecter.go +++ b/expecter.go @@ -18,6 +18,24 @@ type Stmt struct { args []interface{} } +func recordCreateCallback(scope *Scope) { + r, ok := scope.Get("gorm:recorder") + + if !ok { + panic(fmt.Errorf("Expected a recorder to be set, but got none")) + } + + stmt := Stmt{ + kind: "exec", + sql: scope.SQL, + args: scope.SQLVars, + } + + recorder := r.(*Recorder) + + recorder.Record(stmt) +} + func recordQueryCallback(scope *Scope) { r, ok := scope.Get("gorm:recorder") @@ -38,9 +56,6 @@ func recordQueryCallback(scope *Scope) { scope.prepareQuerySQL() stmt.preload = recorder.preload[0].schema - // spew.Printf("_____PRELOADING_____\r\n%s\r\n", stmt.preload) - // spew.Printf("_____SQL_____\r\n%s\r\n", scope.SQL) - // we just want to pop the first element off recorder.preload = recorder.preload[1:] } @@ -67,35 +82,6 @@ func (r *Recorder) Record(stmt Stmt) { r.stmts = append(r.stmts, stmt) } -func getStmtFromLog(values ...interface{}) Stmt { - var statement Stmt - - if len(values) > 1 { - var ( - level = values[0] - ) - - if level == "sql" { - statement.args = values[4].([]interface{}) - statement.sql = values[3].(string) - } - - return statement - } - - return statement -} - -// Print just sets the last recorded SQL statement -// TODO: find a better way to extract SQL from log messages -func (r *Recorder) Print(args ...interface{}) { - statement := getStmtFromLog(args...) - - if statement.sql != "" { - r.stmts = append(r.stmts, statement) - } -} - // GetFirst returns the first recorded sql statement logged. If there are no // statements, false is returned func (r *Recorder) GetFirst() (Stmt, bool) { @@ -138,7 +124,6 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm := &DB{ db: noop, logger: defaultLogger, - logMode: 2, values: map[string]interface{}{}, callbacks: DefaultCallback, dialect: newDialect("sqlmock", noop), @@ -146,6 +131,7 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm.parent = gorm gorm = gorm.Set("gorm:recorder", recorder) + gorm.Callback().Create().After("gorm:create").Register("gorm:record_exec", recordCreateCallback) gorm.Callback().Query().Before("gorm:preload").Register("gorm:record_preload", recordPreloadCallback) gorm.Callback().Query().After("gorm:query").Register("gorm:record_query", recordQueryCallback) gorm.Callback().RowQuery().Before("gorm:row_query").Register("gorm:record_query", recordQueryCallback) @@ -171,6 +157,16 @@ func (h *Expecter) AssertExpectations() error { return h.adapter.AssertExpectations() } +/* CREATE */ + +// Create mocks insertion of a model into the DB +func (h *Expecter) Create(model interface{}) ExpectedExec { + h.gorm.Create(model) + return h.adapter.ExpectExec(h.recorder.stmts[0]) +} + +/* READ */ + // First triggers a Query func (h *Expecter) First(out interface{}, where ...interface{}) ExpectedQuery { h.gorm.First(out, where...) diff --git a/expecter_adapter.go b/expecter_adapter.go index 62929678..81d4cde0 100644 --- a/expecter_adapter.go +++ b/expecter_adapter.go @@ -25,7 +25,7 @@ func init() { // implementations (e.g. go-sqlmock or go-testdb) type Adapter interface { ExpectQuery(stmts ...Stmt) ExpectedQuery - ExpectExec(stmt string) ExpectedExec + ExpectExec(stmt Stmt) ExpectedExec AssertExpectations() error } @@ -56,10 +56,8 @@ func (a *SqlmockAdapter) ExpectQuery(queries ...Stmt) ExpectedQuery { // ExpectExec wraps the underlying mock method for setting a exec // expectation -func (a *SqlmockAdapter) ExpectExec(stmt string) ExpectedExec { - e := a.mocker.ExpectExec(stmt) - - return &SqlmockExec{exec: e} +func (a *SqlmockAdapter) ExpectExec(exec Stmt) ExpectedExec { + return &SqlmockExec{mock: a.mocker, exec: exec} } // AssertExpectations asserts that _all_ expectations for a test have been met diff --git a/expecter_noop.go b/expecter_noop.go index fc9c9d89..04d10881 100644 --- a/expecter_noop.go +++ b/expecter_noop.go @@ -45,12 +45,12 @@ type NoopResult struct{} // LastInsertId is a noop method for satisfying drive.Result func (r NoopResult) LastInsertId() (int64, error) { - return 1, nil + return 0, nil } // RowsAffected is a noop method for satisfying drive.Result func (r NoopResult) RowsAffected() (int64, error) { - return 1, nil + return 0, nil } // NoopRows implements driver.Rows diff --git a/expecter_result.go b/expecter_result.go index f68ec388..dac76276 100644 --- a/expecter_result.go +++ b/expecter_result.go @@ -20,7 +20,8 @@ type ExpectedQuery interface { // return a result. It presents a fluent API for chaining calls to other // expectations type ExpectedExec interface { - Returns(result driver.Result) ExpectedExec + WillSucceed(lastInsertID, rowsAffected int64) ExpectedExec + WillFail(err error) ExpectedExec } // SqlmockQuery implements Query for go-sqlmock @@ -234,14 +235,25 @@ func (q *SqlmockQuery) Returns(out interface{}) ExpectedQuery { // SqlmockExec implements Exec for go-sqlmock type SqlmockExec struct { - exec *sqlmock.ExpectedExec + exec Stmt + mock sqlmock.Sqlmock + scope *Scope } -// Returns accepts a driver.Result. It is passed directly to the underlying +// WillSucceed accepts a two int64s. They are passed directly to the underlying // mock db. Useful for checking DAO behaviour in the event that the incorrect // number of rows are affected by an Exec -func (e *SqlmockExec) Returns(result driver.Result) ExpectedExec { - e.exec = e.exec.WillReturnResult(result) +func (e *SqlmockExec) WillSucceed(lastReturnedID, rowsAffected int64) ExpectedExec { + result := sqlmock.NewResult(lastReturnedID, rowsAffected) + e.mock.ExpectExec(regexp.QuoteMeta(e.exec.sql)).WillReturnResult(result) + + return e +} + +// WillFail simulates returning an Error from an unsuccessful exec +func (e *SqlmockExec) WillFail(err error) ExpectedExec { + result := sqlmock.NewErrorResult(err) + e.mock.ExpectExec(regexp.QuoteMeta(e.exec.sql)).WillReturnResult(result) return e } diff --git a/expecter_test.go b/expecter_test.go index 315f20a5..9b56d16e 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -1,6 +1,7 @@ package gorm_test import ( + "errors" "fmt" "reflect" "testing" @@ -222,10 +223,52 @@ func TestMockPreloadMultiple(t *testing.T) { t.Error(err) } - // spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) - // spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) - if !reflect.DeepEqual(in, out) { t.Error("In and out are not equal") } } + +func TestMockCreateBasic(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + user := User{Name: "jinzhu"} + expect.Create(&user).WillSucceed(1, 1) + rowsAffected := db.Create(&user).RowsAffected + + if rowsAffected != 1 { + t.Errorf("Expected rows affected to be 1 but got %d", rowsAffected) + } + + if user.Id != 1 { + t.Errorf("User id field should be 1, but got %d", user.Id) + } +} + +func TestMockCreateError(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + mockError := errors.New("Could not insert user") + + user := User{Name: "jinzhu"} + expect.Create(&user).WillFail(mockError) + + dbError := db.Create(&user).Error + + if dbError == nil || dbError != mockError { + t.Errorf("Expected *DB.Error to be set, but it was not") + } +} From 607e8f60e498588fb6715f1ff94b2a3c3d135669 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Fri, 24 Nov 2017 17:33:45 +0800 Subject: [PATCH 38/39] Support create and updates --- expecter.go | 31 ++++++++++++++++++-- expecter_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/expecter.go b/expecter.go index cd746436..0bcfb09d 100644 --- a/expecter.go +++ b/expecter.go @@ -18,7 +18,7 @@ type Stmt struct { args []interface{} } -func recordCreateCallback(scope *Scope) { +func recordExecCallback(scope *Scope) { r, ok := scope.Get("gorm:recorder") if !ok { @@ -131,10 +131,11 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm.parent = gorm gorm = gorm.Set("gorm:recorder", recorder) - gorm.Callback().Create().After("gorm:create").Register("gorm:record_exec", recordCreateCallback) + gorm.Callback().Create().After("gorm:create").Register("gorm:record_exec", recordExecCallback) gorm.Callback().Query().Before("gorm:preload").Register("gorm:record_preload", recordPreloadCallback) gorm.Callback().Query().After("gorm:query").Register("gorm:record_query", recordQueryCallback) gorm.Callback().RowQuery().Before("gorm:row_query").Register("gorm:record_query", recordQueryCallback) + gorm.Callback().Update().After("gorm:update").Register("gorm:record_exec", recordExecCallback) return gormDb, &Expecter{adapter: adapter, gorm: gorm, recorder: recorder}, nil } @@ -157,6 +158,12 @@ func (h *Expecter) AssertExpectations() error { return h.adapter.AssertExpectations() } +// Model sets scope.Value +func (h *Expecter) Model(model interface{}) *Expecter { + h.gorm = h.gorm.Model(model) + return h +} + /* CREATE */ // Create mocks insertion of a model into the DB @@ -187,6 +194,26 @@ func (h *Expecter) Preload(column string, conditions ...interface{}) *Expecter { return clone } +/* UPDATE */ + +// Save mocks updating a record in the DB and will trigger db.Exec() +func (h *Expecter) Save(model interface{}) ExpectedExec { + h.gorm.Save(model) + return h.adapter.ExpectExec(h.recorder.stmts[0]) +} + +// Update mocks updating the given attributes in the DB +func (h *Expecter) Update(attrs ...interface{}) ExpectedExec { + h.gorm.Update(attrs...) + return h.adapter.ExpectExec(h.recorder.stmts[0]) +} + +// Updates does the same thing as Update, but with map or struct +func (h *Expecter) Updates(values interface{}, ignoreProtectedAttrs ...bool) ExpectedExec { + h.gorm.Updates(values, ignoreProtectedAttrs...) + return h.adapter.ExpectExec(h.recorder.stmts[0]) +} + /* PRIVATE METHODS */ func (h *Expecter) clone() *Expecter { diff --git a/expecter_test.go b/expecter_test.go index 9b56d16e..e6ac58e6 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -272,3 +272,76 @@ func TestMockCreateError(t *testing.T) { t.Errorf("Expected *DB.Error to be set, but it was not") } } + +func TestMockSaveBasic(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + user := User{Name: "jinzhu"} + expect.Save(&user).WillSucceed(1, 1) + expected := db.Save(&user) + + if err := expect.AssertExpectations(); err != nil { + t.Errorf("Expectations were not met %s", err.Error()) + } + + if expected.RowsAffected != 1 || user.Id != 1 { + t.Errorf("Expected result was not returned") + } +} + +func TestMockUpdateBasic(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + newName := "uhznij" + user := User{Name: "jinzhu"} + + expect.Model(&user).Update("name", newName).WillSucceed(1, 1) + db.Model(&user).Update("name", newName) + + if err := expect.AssertExpectations(); err != nil { + t.Errorf("Expectations were not met %s", err.Error()) + } + + if user.Name != newName { + t.Errorf("Should have name %s but got %s", newName, user.Name) + } +} + +func TestMockUpdatesBasic(t *testing.T) { + db, expect, err := gorm.NewDefaultExpecter() + defer func() { + db.Close() + }() + + if err != nil { + t.Fatal(err) + } + + user := User{Name: "jinzhu", Age: 18} + updated := User{Name: "jinzhu", Age: 88} + + expect.Model(&user).Updates(updated).WillSucceed(1, 1) + db.Model(&user).Updates(updated) + + if err := expect.AssertExpectations(); err != nil { + t.Errorf("Expectations were not met %s", err.Error()) + } + + if user.Age != updated.Age { + t.Errorf("Should have age %d but got %d", user.Age, updated.Age) + } +} From d630799e906b41467a47b801ec24b892e71a47e0 Mon Sep 17 00:00:00 2001 From: Ian Tan Date: Sun, 26 Nov 2017 15:14:38 +0800 Subject: [PATCH 39/39] Minor cleanup --- expecter.go | 8 ++++---- expecter_test.go | 51 ++++++++++++------------------------------------ 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/expecter.go b/expecter.go index 0bcfb09d..6dc28996 100644 --- a/expecter.go +++ b/expecter.go @@ -43,17 +43,16 @@ func recordQueryCallback(scope *Scope) { panic(fmt.Errorf("Expected a recorder to be set, but got none")) } + recorder := r.(*Recorder) + stmt := Stmt{ kind: "query", sql: scope.SQL, args: scope.SQLVars, } - recorder := r.(*Recorder) - if len(recorder.preload) > 0 { // this will cause the scope.SQL to mutate to the preload query - scope.prepareQuerySQL() stmt.preload = recorder.preload[0].schema // we just want to pop the first element off @@ -71,6 +70,7 @@ func recordPreloadCallback(scope *Scope) { if !ok { panic(fmt.Errorf("Expected a recorder to be set, but got none")) } + if len(scope.Search.preload) > 0 { // spew.Printf("callback:preload\r\n%s\r\n", spew.Sdump(scope.Search.preload)) recorder.(*Recorder).preload = scope.Search.preload @@ -134,7 +134,7 @@ func NewDefaultExpecter() (*DB, *Expecter, error) { gorm.Callback().Create().After("gorm:create").Register("gorm:record_exec", recordExecCallback) gorm.Callback().Query().Before("gorm:preload").Register("gorm:record_preload", recordPreloadCallback) gorm.Callback().Query().After("gorm:query").Register("gorm:record_query", recordQueryCallback) - gorm.Callback().RowQuery().Before("gorm:row_query").Register("gorm:record_query", recordQueryCallback) + gorm.Callback().RowQuery().After("gorm:row_query").Register("gorm:record_query", recordQueryCallback) gorm.Callback().Update().After("gorm:update").Register("gorm:record_exec", recordExecCallback) return gormDb, &Expecter{adapter: adapter, gorm: gorm, recorder: recorder}, nil diff --git a/expecter_test.go b/expecter_test.go index e6ac58e6..64dfc02d 100644 --- a/expecter_test.go +++ b/expecter_test.go @@ -2,7 +2,6 @@ package gorm_test import ( "errors" - "fmt" "reflect" "testing" @@ -11,9 +10,8 @@ import ( func TestNewDefaultExpecter(t *testing.T) { db, _, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + //lint:ignore SA5001 just a mock + defer db.Close() if err != nil { t.Fatal(err) @@ -22,6 +20,7 @@ func TestNewDefaultExpecter(t *testing.T) { func TestNewCustomExpecter(t *testing.T) { db, _, err := gorm.NewExpecter(gorm.NewSqlmockAdapter, "sqlmock", "mock_gorm_dsn") + //lint:ignore SA5001 just a mock defer db.Close() if err != nil { @@ -36,7 +35,6 @@ func TestQuery(t *testing.T) { t.Fatal(err) } - fmt.Println("Got here") expect.First(&User{}) db.First(&User{}) @@ -97,9 +95,7 @@ func TestFindStructDest(t *testing.T) { func TestFindSlice(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -122,9 +118,7 @@ func TestFindSlice(t *testing.T) { func TestMockPreloadHasMany(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -148,9 +142,7 @@ func TestMockPreloadHasMany(t *testing.T) { func TestMockPreloadHasOne(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -173,9 +165,7 @@ func TestMockPreloadHasOne(t *testing.T) { func TestMockPreloadMany2Many(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -192,9 +182,6 @@ func TestMockPreloadMany2Many(t *testing.T) { t.Error(err) } - // spew.Printf("______IN______\r\n%s\r\n", spew.Sdump(in)) - // spew.Printf("______OUT______\r\n%s\r\n", spew.Sdump(out)) - if !reflect.DeepEqual(in, out) { t.Error("In and out are not equal") } @@ -202,9 +189,7 @@ func TestMockPreloadMany2Many(t *testing.T) { func TestMockPreloadMultiple(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -230,9 +215,7 @@ func TestMockPreloadMultiple(t *testing.T) { func TestMockCreateBasic(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -253,9 +236,7 @@ func TestMockCreateBasic(t *testing.T) { func TestMockCreateError(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -275,9 +256,7 @@ func TestMockCreateError(t *testing.T) { func TestMockSaveBasic(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -298,9 +277,7 @@ func TestMockSaveBasic(t *testing.T) { func TestMockUpdateBasic(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err) @@ -323,9 +300,7 @@ func TestMockUpdateBasic(t *testing.T) { func TestMockUpdatesBasic(t *testing.T) { db, expect, err := gorm.NewDefaultExpecter() - defer func() { - db.Close() - }() + defer db.Close() if err != nil { t.Fatal(err)