From 93baa1bc533a35e6dc5989b969f1072646de820e Mon Sep 17 00:00:00 2001 From: Erik Dubbelboer Date: Wed, 1 Jun 2016 16:09:12 +0200 Subject: [PATCH 1/9] Update import after user renamed on github Looks like user cenkalti renamed his account to cenk. Github automatically redirects but it's better to update the import path. --- cluster.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cluster.go b/cluster.go index b92f139e..22e4f985 100644 --- a/cluster.go +++ b/cluster.go @@ -8,7 +8,7 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/cenkalti/backoff" + "github.com/cenk/backoff" "github.com/hailocab/go-hostpool" ) From 5cf14ef7574f09d1109d4c134173403c4cc87d13 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Tue, 14 Jun 2016 22:31:16 +0100 Subject: [PATCH 2/9] Initial attempt at adding mocking --- mock.go | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++ mock_test.go | 67 ++++++++++++++ query.go | 22 ++++- 3 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 mock.go create mode 100644 mock_test.go diff --git a/mock.go b/mock.go new file mode 100644 index 00000000..a3532be9 --- /dev/null +++ b/mock.go @@ -0,0 +1,254 @@ +package gorethink + +import ( + "bytes" + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/stretchr/testify/assert" +) + +// Mocking is based on the amazing package github.com/stretchr/testify + +type MockQuery struct { + parent *Mock + + // Holds the query and term + Query Query + + // Holds the JSON representation of query + BuiltQuery []byte + + // Holds the response that should be returned when this method is called. + Response interface{} + + // Holds the error that should be returned when this method is called. + Error error + + // The number of times to return the return arguments when setting + // expectations. 0 means to always return the value. + Repeatability int + + // Holds a channel that will be used to block the Return until it either + // recieves a message or is closed. nil means it returns immediately. + WaitFor <-chan time.Time + + // Amount of times this call has been called + count int +} + +func newMockQuery(parent *Mock, q Query) *MockQuery { + // Build and marshal term + builtQuery, err := json.Marshal(q.build()) + if err != nil { + panic(fmt.Sprintf("Failed to build query: %s", err)) + } + + return &MockQuery{ + parent: parent, + Query: q, + BuiltQuery: builtQuery, + Response: make([]interface{}, 0), + Repeatability: 0, + WaitFor: nil, + } +} + +func newMockQueryFromTerm(parent *Mock, t Term, opts map[string]interface{}) *MockQuery { + q, err := parent.newQuery(t, opts) + if err != nil { + panic(fmt.Sprintf("Failed to build query: %s", err)) + } + + return newMockQuery(parent, q) +} + +func (mq *MockQuery) lock() { + mq.parent.mu.Lock() +} + +func (mq *MockQuery) unlock() { + mq.parent.mu.Unlock() +} + +// Return specifies the return arguments for the expectation. +// +// Mock.On("DoSomething").Return(nil, errors.New("failed")) +func (mq *MockQuery) Return(response interface{}, err error) *MockQuery { + mq.lock() + defer mq.unlock() + + mq.Response = response + mq.Error = err + + return mq +} + +// Once indicates that that the mock should only return the value once. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once() +func (mq *MockQuery) Once() *MockQuery { + return mq.Times(1) +} + +// Twice indicates that that the mock should only return the value twice. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice() +func (mq *MockQuery) Twice() *MockQuery { + return mq.Times(2) +} + +// Times indicates that that the mock should only return the indicated number +// of times. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5) +func (mq *MockQuery) Times(i int) *MockQuery { + mq.lock() + defer mq.unlock() + mq.Repeatability = i + return mq +} + +// WaitUntil sets the channel that will block the mock's return until its closed +// or a message is received. +// +// Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second)) +func (mq *MockQuery) WaitUntil(w <-chan time.Time) *MockQuery { + mq.lock() + defer mq.unlock() + mq.WaitFor = w + return mq +} + +// After sets how long to block until the call returns +// +// Mock.On("MyMethod", arg1, arg2).After(time.Second) +func (mq *MockQuery) After(d time.Duration) *MockQuery { + return mq.WaitUntil(time.After(d)) +} + +// On chains a new expectation description onto the mocked interface. This +// allows syntax like. +// +// Mock. +// On("MyMethod", 1).Return(nil). +// On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error")) +func (mq *MockQuery) On(t Term) *MockQuery { + return mq.parent.On(t) +} + +type Mock struct { + mu sync.Mutex + opts ConnectOpts + + ExpectedQueries []*MockQuery + Queries []MockQuery +} + +func NewMock(opts ...ConnectOpts) *Mock { + m := &Mock{ + ExpectedQueries: make([]*MockQuery, 0), + Queries: make([]MockQuery, 0), + } + + if len(opts) > 0 { + m.opts = opts[0] + } + + return m +} + +func (m *Mock) On(t Term, opts ...map[string]interface{}) *MockQuery { + var qopts map[string]interface{} + if len(opts) > 0 { + qopts = opts[0] + } + + m.mu.Lock() + defer m.mu.Unlock() + mq := newMockQueryFromTerm(m, t, qopts) + m.ExpectedQueries = append(m.ExpectedQueries, mq) + return mq +} + +func (m *Mock) IsConnected() bool { + return true +} + +func (m *Mock) Query(q Query) (*Cursor, error) { + found, query := m.findExpectedQuery(q) + + if found < 0 { + panic(fmt.Sprintf("gorethink: mock: This query was unexpected:\n\t\t%s\n\tat: %s", q.Term.String(), assert.CallerInfo())) + } else { + m.mu.Lock() + switch { + case query.Repeatability == 1: + query.Repeatability = -1 + query.count++ + + case query.Repeatability > 1: + query.Repeatability-- + query.count++ + + case query.Repeatability == 0: + query.count++ + } + m.mu.Unlock() + } + + // add the query + m.mu.Lock() + m.Queries = append(m.Queries, *newMockQuery(m, q)) + m.mu.Unlock() + + // block if specified + if query.WaitFor != nil { + <-query.WaitFor + } + + // Return error without building cursor if non-nil + if query.Error != nil { + return nil, query.Error + } + + // Build cursor and return + c := newCursor(nil, "", query.Query.Token, query.Query.Term, query.Query.Opts) + c.buffer = append(c.buffer, query.Response) + c.finished = true + c.fetching = false + c.isAtom = true + + return c, nil +} + +func (m *Mock) Exec(q Query) error { + _, err := m.Query(q) + + return err +} + +func (m *Mock) newQuery(t Term, opts map[string]interface{}) (Query, error) { + return newQuery(t, opts, &m.opts) +} + +func (m *Mock) findExpectedQuery(q Query) (int, *MockQuery) { + // Build and marshal query + builtQuery, err := json.Marshal(q.build()) + if err != nil { + panic(fmt.Sprintf("Failed to build query: %s", err)) + } + + m.mu.Lock() + defer m.mu.Unlock() + + for i, query := range m.ExpectedQueries { + if bytes.Equal(query.BuiltQuery, builtQuery) && query.Repeatability > -1 { + return i, query + } + } + + return -1, nil +} diff --git a/mock_test.go b/mock_test.go new file mode 100644 index 00000000..06f353dc --- /dev/null +++ b/mock_test.go @@ -0,0 +1,67 @@ +package gorethink + +import ( + "fmt" + + test "gopkg.in/check.v1" +) + +func (s *RethinkSuite) TestMockExecSuccess(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test").Insert(map[string]string{ + "id": "mocked", + })).Return(nil, nil) + + err := DB("test").Table("test").Insert(map[string]string{ + "id": "mocked", + }).Exec(mock) + c.Assert(err, test.IsNil) +} + +func (s *RethinkSuite) TestMockExecFail(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test").Insert(map[string]string{ + "id": "mocked", + })).Return(nil, fmt.Errorf("Expected error")) + + err := DB("test").Table("test").Insert(map[string]string{ + "id": "mocked", + }).Exec(mock) + c.Assert(err, test.NotNil) +} + +func (s *RethinkSuite) TestMockRunSuccessSingle(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test").Get("mocked")).Return(map[string]interface{}{ + "id": "mocked", + }, nil) + + res, err := DB("test").Table("test").Get("mocked").Run(mock) + c.Assert(err, test.IsNil) + + var response interface{} + err = res.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, jsonEquals, map[string]interface{}{"id": "mocked"}) + + res.Close() +} + +func (s *RethinkSuite) TestMockRunSuccessMultiple(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test")).Return([]interface{}{ + map[string]interface{}{"id": "mocked"}, + }, nil) + + res, err := DB("test").Table("test").Run(mock) + c.Assert(err, test.IsNil) + + var response []interface{} + err = res.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, jsonEquals, []interface{}{map[string]interface{}{"id": "mocked"}}) + + res.Close() +} diff --git a/query.go b/query.go index 8c04dbce..7a5bd115 100644 --- a/query.go +++ b/query.go @@ -171,6 +171,14 @@ type OptArgs interface { toMap() map[string]interface{} } +type QueryExecutor interface { + IsConnected() bool + Query(Query) (*Cursor, error) + Exec(Query) error + + newQuery(t Term, opts map[string]interface{}) (Query, error) +} + // WriteResponse is a helper type used when dealing with the response of a // write query. It is also returned by the RunWrite function. type WriteResponse struct { @@ -236,7 +244,7 @@ func (o *RunOpts) toMap() map[string]interface{} { // for rows.Next(&doc) { // // Do something with document // } -func (t Term) Run(s *Session, optArgs ...RunOpts) (*Cursor, error) { +func (t Term) Run(s QueryExecutor, optArgs ...RunOpts) (*Cursor, error) { opts := map[string]interface{}{} if len(optArgs) >= 1 { opts = optArgs[0].toMap() @@ -261,7 +269,7 @@ func (t Term) Run(s *Session, optArgs ...RunOpts) (*Cursor, error) { // If an error occurs when running the write query the first error is returned. // // res, err := r.DB("database").Table("table").Insert(doc).RunWrite(sess) -func (t Term) RunWrite(s *Session, optArgs ...RunOpts) (WriteResponse, error) { +func (t Term) RunWrite(s QueryExecutor, optArgs ...RunOpts) (WriteResponse, error) { var response WriteResponse res, err := t.Run(s, optArgs...) @@ -285,7 +293,7 @@ func (t Term) RunWrite(s *Session, optArgs ...RunOpts) (WriteResponse, error) { // and reads one response from the cursor before closing it. // // It returns any errors encountered from running the query or reading the response -func (t Term) ReadOne(dest interface{}, s *Session, optArgs ...RunOpts) error { +func (t Term) ReadOne(dest interface{}, s QueryExecutor, optArgs ...RunOpts) error { res, err := t.Run(s, optArgs...) if err != nil { return err @@ -297,7 +305,7 @@ func (t Term) ReadOne(dest interface{}, s *Session, optArgs ...RunOpts) error { // and reads all of the responses from the cursor before closing it. // // It returns any errors encountered from running the query or reading the responses -func (t Term) ReadAll(dest interface{}, s *Session, optArgs ...RunOpts) error { +func (t Term) ReadAll(dest interface{}, s QueryExecutor, optArgs ...RunOpts) error { res, err := t.Run(s, optArgs...) if err != nil { return err @@ -342,12 +350,16 @@ func (o *ExecOpts) toMap() map[string]interface{} { // err := r.DB("database").Table("table").Insert(doc).Exec(sess, r.ExecOpts{ // NoReply: true, // }) -func (t Term) Exec(s *Session, optArgs ...ExecOpts) error { +func (t Term) Exec(s QueryExecutor, optArgs ...ExecOpts) error { opts := map[string]interface{}{} if len(optArgs) >= 1 { opts = optArgs[0].toMap() } + if s == nil || !s.IsConnected() { + return ErrConnectionClosed + } + q, err := s.newQuery(t, opts) if err != nil { return err From bfd7ca92b0b4fc8c72d535899b0a74c8cac52c58 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 18 Jun 2016 15:36:00 +0100 Subject: [PATCH 3/9] Finished off implementation of mocking --- mock.go | 154 +++++++++++++++++++++++++++++++++++++++++------ mock_test.go | 119 +++++++++++++++++++++++++++++++++++- query_control.go | 4 +- 3 files changed, 254 insertions(+), 23 deletions(-) diff --git a/mock.go b/mock.go index a3532be9..a486e4e6 100644 --- a/mock.go +++ b/mock.go @@ -6,12 +6,19 @@ import ( "fmt" "sync" "time" - - "github.com/stretchr/testify/assert" ) // Mocking is based on the amazing package github.com/stretchr/testify +// testingT is an interface wrapper around *testing.T +type testingT interface { + Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + FailNow() +} + +// MockQuery represents a mocked query and is used for setting expectations, +// as well as recording activity. type MockQuery struct { parent *Mock @@ -21,10 +28,10 @@ type MockQuery struct { // Holds the JSON representation of query BuiltQuery []byte - // Holds the response that should be returned when this method is called. + // Holds the response that should be returned when this method is executed. Response interface{} - // Holds the error that should be returned when this method is called. + // Holds the error that should be returned when this method is executed. Error error // The number of times to return the return arguments when setting @@ -35,8 +42,8 @@ type MockQuery struct { // recieves a message or is closed. nil means it returns immediately. WaitFor <-chan time.Time - // Amount of times this call has been called - count int + // Amount of times this query has been executed + executed int } func newMockQuery(parent *Mock, q Query) *MockQuery { @@ -75,7 +82,7 @@ func (mq *MockQuery) unlock() { // Return specifies the return arguments for the expectation. // -// Mock.On("DoSomething").Return(nil, errors.New("failed")) +// mock.On(r.Table("test")).Return(nil, errors.New("failed")) func (mq *MockQuery) Return(response interface{}, err error) *MockQuery { mq.lock() defer mq.unlock() @@ -88,14 +95,14 @@ func (mq *MockQuery) Return(response interface{}, err error) *MockQuery { // Once indicates that that the mock should only return the value once. // -// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once() +// mock.On(r.Table("test")).Return(result, nil).Once() func (mq *MockQuery) Once() *MockQuery { return mq.Times(1) } // Twice indicates that that the mock should only return the value twice. // -// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice() +// mock.On(r.Table("test")).Return(result, nil).Twice() func (mq *MockQuery) Twice() *MockQuery { return mq.Times(2) } @@ -103,7 +110,7 @@ func (mq *MockQuery) Twice() *MockQuery { // Times indicates that that the mock should only return the indicated number // of times. // -// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5) +// mock.On(r.Table("test")).Return(result, nil).Times(5) func (mq *MockQuery) Times(i int) *MockQuery { mq.lock() defer mq.unlock() @@ -114,7 +121,7 @@ func (mq *MockQuery) Times(i int) *MockQuery { // WaitUntil sets the channel that will block the mock's return until its closed // or a message is received. // -// Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second)) +// mock.On(r.Table("test")).WaitUntil(time.After(time.Second)) func (mq *MockQuery) WaitUntil(w <-chan time.Time) *MockQuery { mq.lock() defer mq.unlock() @@ -122,9 +129,9 @@ func (mq *MockQuery) WaitUntil(w <-chan time.Time) *MockQuery { return mq } -// After sets how long to block until the call returns +// After sets how long to block until the query returns // -// Mock.On("MyMethod", arg1, arg2).After(time.Second) +// mock.On(r.Table("test")).After(time.Second) func (mq *MockQuery) After(d time.Duration) *MockQuery { return mq.WaitUntil(time.After(d)) } @@ -133,12 +140,22 @@ func (mq *MockQuery) After(d time.Duration) *MockQuery { // allows syntax like. // // Mock. -// On("MyMethod", 1).Return(nil). -// On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error")) +// On(r.Table("test")).Return(result, nil). +// On(r.Table("test2")).Return(nil, errors.New("Some Error")) func (mq *MockQuery) On(t Term) *MockQuery { return mq.parent.On(t) } +// Mock is used to mock query execution and verify that the expected queries are +// being executed. Mocks are used by creating an instance using NewMock and then +// passing this when running your queries instead of a session. For example: +// +// mock := r.NewMock() +// mock.on(r.Table("test")).Return([]interface{}{data}, nil) +// +// cursor, err := r.Table("test").Run(mock) +// +// mock.AssertExpectations(t) type Mock struct { mu sync.Mutex opts ConnectOpts @@ -147,6 +164,9 @@ type Mock struct { Queries []MockQuery } +// NewMock creates an instance of Mock, you can optionally pass ConnectOpts to +// the function, if passed any mocked query will be generated using those +// options. func NewMock(opts ...ConnectOpts) *Mock { m := &Mock{ ExpectedQueries: make([]*MockQuery, 0), @@ -160,6 +180,10 @@ func NewMock(opts ...ConnectOpts) *Mock { return m } +// On starts a description of an expectation of the specified query +// being executed. +// +// mock.On(r.Table("test")) func (m *Mock) On(t Term, opts ...map[string]interface{}) *MockQuery { var qopts map[string]interface{} if len(opts) > 0 { @@ -173,6 +197,75 @@ func (m *Mock) On(t Term, opts ...map[string]interface{}) *MockQuery { return mq } +// AssertExpectations asserts that everything specified with On and Return was +// in fact executed as expected. Queries may have been executed in any order. +func (m *Mock) AssertExpectations(t testingT) bool { + var somethingMissing bool + var failedExpectations int + + // iterate through each expectation + expectedQueries := m.expectedQueries() + for _, expectedQuery := range expectedQueries { + if !m.queryWasExecuted(expectedQuery) && expectedQuery.executed == 0 { + somethingMissing = true + failedExpectations++ + t.Logf("❌\t%s", expectedQuery.Query.Term.String()) + } else { + m.mu.Lock() + if expectedQuery.Repeatability > 0 { + somethingMissing = true + failedExpectations++ + } else { + t.Logf("✅\t%s", expectedQuery.Query.Term.String()) + } + m.mu.Unlock() + } + } + + if somethingMissing { + t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe query you are testing needs to be executed %d more times(s).", len(expectedQueries)-failedExpectations, len(expectedQueries), failedExpectations) + } + + return !somethingMissing +} + +// AssertNumberOfExecutions asserts that the query was executed expectedExecutions times. +func (m *Mock) AssertNumberOfExecutions(t testingT, expectedQuery *MockQuery, expectedExecutions int) bool { + var actualExecutions int + for _, query := range m.queries() { + if bytes.Equal(query.BuiltQuery, expectedQuery.BuiltQuery) { + actualExecutions++ + } + } + + if expectedExecutions != actualExecutions { + t.Errorf("Expected number of executions (%d) does not match the actual number of executions (%d).", expectedExecutions, actualExecutions) + return false + } + + return true +} + +// AssertExecuted asserts that the method was executed. +// It can produce a false result when an argument is a pointer type and the underlying value changed after executing the mocked method. +func (m *Mock) AssertExecuted(t testingT, expectedQuery *MockQuery) bool { + if !m.queryWasExecuted(expectedQuery) { + t.Errorf("The query \"%s\" should have been executed, but was not.", expectedQuery.Query.Term.String()) + return false + } + return true +} + +// AssertNotExecuted asserts that the method was not executed. +// It can produce a false result when an argument is a pointer type and the underlying value changed after executing the mocked method. +func (m *Mock) AssertNotExecuted(t testingT, expectedQuery *MockQuery) bool { + if m.queryWasExecuted(expectedQuery) { + t.Errorf("The query \"%s\" was executed, but should NOT have been.", expectedQuery.Query.Term.String()) + return false + } + return true +} + func (m *Mock) IsConnected() bool { return true } @@ -181,20 +274,20 @@ func (m *Mock) Query(q Query) (*Cursor, error) { found, query := m.findExpectedQuery(q) if found < 0 { - panic(fmt.Sprintf("gorethink: mock: This query was unexpected:\n\t\t%s\n\tat: %s", q.Term.String(), assert.CallerInfo())) + panic(fmt.Sprintf("gorethink: mock: This query was unexpected:\n\t\t%s", q.Term.String())) } else { m.mu.Lock() switch { case query.Repeatability == 1: query.Repeatability = -1 - query.count++ + query.executed++ case query.Repeatability > 1: query.Repeatability-- - query.count++ + query.executed++ case query.Repeatability == 0: - query.count++ + query.executed++ } m.mu.Unlock() } @@ -252,3 +345,26 @@ func (m *Mock) findExpectedQuery(q Query) (int, *MockQuery) { return -1, nil } + +func (m *Mock) queryWasExecuted(expectedQuery *MockQuery) bool { + for _, query := range m.queries() { + if bytes.Equal(query.BuiltQuery, expectedQuery.BuiltQuery) { + return true + } + } + + // we didn't find the expected query + return false +} + +func (m *Mock) expectedQueries() []*MockQuery { + m.mu.Lock() + defer m.mu.Unlock() + return append([]*MockQuery{}, m.ExpectedQueries...) +} + +func (m *Mock) queries() []MockQuery { + m.mu.Lock() + defer m.mu.Unlock() + return append([]MockQuery{}, m.Queries...) +} diff --git a/mock_test.go b/mock_test.go index 06f353dc..388dd061 100644 --- a/mock_test.go +++ b/mock_test.go @@ -16,6 +16,7 @@ func (s *RethinkSuite) TestMockExecSuccess(c *test.C) { "id": "mocked", }).Exec(mock) c.Assert(err, test.IsNil) + mock.AssertExpectations(c) } func (s *RethinkSuite) TestMockExecFail(c *test.C) { @@ -28,9 +29,10 @@ func (s *RethinkSuite) TestMockExecFail(c *test.C) { "id": "mocked", }).Exec(mock) c.Assert(err, test.NotNil) + mock.AssertExpectations(c) } -func (s *RethinkSuite) TestMockRunSuccessSingle(c *test.C) { +func (s *RethinkSuite) TestMockRunSuccessSingleResult(c *test.C) { mock := NewMock() mock.On(DB("test").Table("test").Get("mocked")).Return(map[string]interface{}{ "id": "mocked", @@ -44,11 +46,12 @@ func (s *RethinkSuite) TestMockRunSuccessSingle(c *test.C) { c.Assert(err, test.IsNil) c.Assert(response, jsonEquals, map[string]interface{}{"id": "mocked"}) + mock.AssertExpectations(c) res.Close() } -func (s *RethinkSuite) TestMockRunSuccessMultiple(c *test.C) { +func (s *RethinkSuite) TestMockRunSuccessMultipleResults(c *test.C) { mock := NewMock() mock.On(DB("test").Table("test")).Return([]interface{}{ map[string]interface{}{"id": "mocked"}, @@ -62,6 +65,118 @@ func (s *RethinkSuite) TestMockRunSuccessMultiple(c *test.C) { c.Assert(err, test.IsNil) c.Assert(response, jsonEquals, []interface{}{map[string]interface{}{"id": "mocked"}}) + mock.AssertExpectations(c) res.Close() } + +func (s *RethinkSuite) TestMockRunMissingMock(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test")).Return([]interface{}{ + map[string]interface{}{"id": "mocked"}, + }, nil).Once() + + c.Assert(func() { + c.Assert(DB("test").Table("test").Exec(mock), test.IsNil) + c.Assert(DB("test").Table("test").Exec(mock), test.IsNil) + }, test.PanicMatches, ""+ + "gorethink: mock: This query was unexpected:(?s:.*)") + mock.AssertExpectations(c) +} + +func (s *RethinkSuite) TestMockRunMissingQuery(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test")).Return([]interface{}{ + map[string]interface{}{"id": "mocked"}, + }, nil).Twice() + + c.Assert(DB("test").Table("test").Exec(mock), test.IsNil) + + t := &simpleTestingT{} + mock.AssertExpectations(t) + + c.Assert(t.Failed(), test.Equals, true) +} + +func (s *RethinkSuite) TestMockRunMissingQuerySingle(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test")).Return([]interface{}{ + map[string]interface{}{"id": "mocked"}, + }, nil).Once() + + t := &simpleTestingT{} + mock.AssertExpectations(t) + + c.Assert(t.Failed(), test.Equals, true) +} + +func (s *RethinkSuite) TestMockRunMissingQueryMultiple(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test")).Return([]interface{}{ + map[string]interface{}{"id": "mocked"}, + }, nil).Twice() + + c.Assert(DB("test").Table("test").Exec(mock), test.IsNil) + + t := &simpleTestingT{} + mock.AssertExpectations(t) + + c.Assert(t.Failed(), test.Equals, true) +} + +func (s *RethinkSuite) TestMockRunMutlipleQueries(c *test.C) { + mock := NewMock() + mock.On(DB("test").Table("test").Get("mocked1")).Return(map[string]interface{}{ + "id": "mocked1", + }, nil).Times(2) + mock.On(DB("test").Table("test").Get("mocked2")).Return(map[string]interface{}{ + "id": "mocked2", + }, nil).Times(1) + + var response interface{} + + // Query 1 + res, err := DB("test").Table("test").Get("mocked1").Run(mock) + c.Assert(err, test.IsNil) + + err = res.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, jsonEquals, map[string]interface{}{"id": "mocked1"}) + + // Query 2 + res, err = DB("test").Table("test").Get("mocked1").Run(mock) + c.Assert(err, test.IsNil) + + err = res.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, jsonEquals, map[string]interface{}{"id": "mocked1"}) + + // Query 3 + res, err = DB("test").Table("test").Get("mocked2").Run(mock) + c.Assert(err, test.IsNil) + + err = res.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, jsonEquals, map[string]interface{}{"id": "mocked2"}) + + mock.AssertExpectations(c) +} + +type simpleTestingT struct { + failed bool +} + +func (t *simpleTestingT) Logf(format string, args ...interface{}) { +} +func (t *simpleTestingT) Errorf(format string, args ...interface{}) { + t.failed = true +} +func (t *simpleTestingT) FailNow() { + t.failed = true +} +func (t *simpleTestingT) Failed() bool { + return t.failed +} diff --git a/query_control.go b/query_control.go index f02aa798..3139fdb3 100644 --- a/query_control.go +++ b/query_control.go @@ -342,8 +342,8 @@ func (t Term) Info(args ...interface{}) Term { return constructMethodTerm(t, "Info", p.Term_INFO, args, map[string]interface{}{}) } -// Return a UUID (universally unique identifier), a string that can be used as a -// unique ID. If a string is passed to uuid as an argument, the UUID will be +// UUID returns a UUID (universally unique identifier), a string that can be used +// as a unique ID. If a string is passed to uuid as an argument, the UUID will be // deterministic, derived from the string’s SHA-1 hash. func UUID(args ...interface{}) Term { return constructRootTerm("UUID", p.Term_UUID, args, map[string]interface{}{}) From 02784a47faded09c511d4b50f7b651696ed6d3bb Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 18 Jun 2016 15:58:28 +0100 Subject: [PATCH 4/9] Updated readme and changelog --- CHANGELOG.md | 10 ++++++++++ README.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a831007..18cb65f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added + + - Added ability to mock queries based on the library github.com/stretchr/testify + + Added the `QueryExecutor` interface and changed query runner methods (`Run`/`Exec`) to accept this type instead of `*Session`, `Session` will still be accepted as it implements the `QueryExecutor` interface. + + Added the `NewMock` function to create a mock query executor + + Queries can be mocked using `On` and `Return`, `Mock` also contains functions for asserting that the required mocked queries were executed. + + For more information about how to mock queries see the readme and tests in `mock_test.go`. + ## v2.0.4 - 2016-05-22 ### Changed diff --git a/README.md b/README.md index 38dd9c1f..4658d4fe 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,41 @@ Alternatively if you wish to modify the logging behaviour you can modify the log r.Log.Out = ioutil.Discard ``` +## Mocking + +The driver includes the ability to mock queries meaning that you can test your code without needing to talk to a real RethinkDB cluster, this is perfect for ensuring that your application has high unit test coverage. + +To write tests with mocking you should create an instance of `Mock` and then setup expectations using `On` and `Return`. Expectations allow you to define what results should be returned when a known query is executed, they are configured by passing the query term you want to mock to `On` and then the response and error to `Return`, if a non-nil error is passed to `Return` then any time that query is executed the error will be returned, if no error is passed then a cursor will be built using the value passed to `Return`. Once all your expectations have been created you should then execute you queries using the `Mock` instead of a `Session`. + +Here is an example that shows how to mock a query that returns multiple rows and the resulting cursor can be used as normal. + +```go +func TestSomething(t *testing.T) { + mock := r.NewMock() + mock.on(r.Table("people")).Return([]interface{}{ + map[string]interface{}{"id": 1, "name": "John Smith"}, + map[string]interface{}{"id": 2, "name": "Jane Smith"}, + }, nil) + + cursor, err := r.Table("people").Run(mock) + if err != nil { + t.Errorf(err) + } + + var rows []interface{} + err := res.All(&rows) + if err != nil { + t.Errorf(err) + } + + // Test result of rows + + mock.AssertExpectations(t) +} +``` + +The mocking implementation is based on amazing https://github.com/stretchr/testify library, thanks to @stretchr for their awesome work! + ## Benchmarks Everyone wants their project's benchmarks to be speedy. And while we know that rethinkDb and the gorethink driver are quite fast, our primary goal is for our benchmarks to be correct. They are designed to give you, the user, an accurate picture of writes per second (w/s). If you come up with a accurate test that meets this aim, submit a pull request please. From 95c428acdda530decd77b5f467f854466f129a6c Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Wed, 25 May 2016 21:14:43 +0100 Subject: [PATCH 5/9] Change query retries to use a different host --- CHANGELOG.md | 1 + cluster.go | 69 ++++++++++++++++++++++++++++++++++---------- pool.go | 80 ++++++++++++++++------------------------------------ session.go | 5 ++++ utils.go | 14 +++++++++ 5 files changed, 99 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cb65f0..ba0cefa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Changed `Connect` to return the reason for connections failing (instead of just "no connections were made when creating the session") + - Changed how queries are retried internally, previously when a query failed due to an issue with the connection a new connection was picked from the connection pool and the query was retried, now the driver will attempt to retry the query with a new host (and connection). This should make applications connecting to a multi-node cluster more reliable. ### Fixed - Fixed queries not being retried when using `Query()`, queries are now retried if the request failed due to a bad connection. diff --git a/cluster.go b/cluster.go index b92f139e..92e810e8 100644 --- a/cluster.go +++ b/cluster.go @@ -58,37 +58,68 @@ func NewCluster(hosts []Host, opts *ConnectOpts) (*Cluster, error) { // Query executes a ReQL query using the cluster to connect to the database func (c *Cluster) Query(q Query) (cursor *Cursor, err error) { - node, hpr, err := c.GetNextNode() - if err != nil { - return nil, err + for i := 0; i < c.numRetries(); i++ { + var node *Node + var hpr hostpool.HostPoolResponse + + node, hpr, err = c.GetNextNode() + if err != nil { + return nil, err + } + + cursor, err = node.Query(q) + hpr.Mark(err) + + if !shouldRetryQuery(q, err) { + break + } } - cursor, err = node.Query(q) - hpr.Mark(err) return cursor, err } // Exec executes a ReQL query using the cluster to connect to the database func (c *Cluster) Exec(q Query) (err error) { - node, hpr, err := c.GetNextNode() - if err != nil { - return err + for i := 0; i < c.numRetries(); i++ { + var node *Node + var hpr hostpool.HostPoolResponse + + node, hpr, err = c.GetNextNode() + if err != nil { + return err + } + + err = node.Exec(q) + hpr.Mark(err) + + if !shouldRetryQuery(q, err) { + break + } } - err = node.Exec(q) - hpr.Mark(err) return err } // Server returns the server name and server UUID being used by a connection. func (c *Cluster) Server() (response ServerResponse, err error) { - node, hpr, err := c.GetNextNode() - if err != nil { - return ServerResponse{}, err + for i := 0; i < c.numRetries(); i++ { + var node *Node + var hpr hostpool.HostPoolResponse + + node, hpr, err = c.GetNextNode() + if err != nil { + return ServerResponse{}, err + } + + response, err = node.Server() + hpr.Mark(err) + + // This query should not fail so retry if any error is detected + if err == nil { + break + } } - response, err = node.Server() - hpr.Mark(err) return response, err } @@ -473,3 +504,11 @@ func (c *Cluster) removeNode(nodeID string) { func (c *Cluster) nextNodeIndex() int64 { return atomic.AddInt64(&c.nodeIndex, 1) } + +func (c *Cluster) numRetries() int { + if n := c.opts.NumRetries; n > 0 { + return n + } + + return 3 +} diff --git a/pool.go b/pool.go index b49ea7a3..7c65e0a7 100644 --- a/pool.go +++ b/pool.go @@ -9,8 +9,6 @@ import ( "gopkg.in/fatih/pool.v2" ) -const maxBadConnRetries = 3 - var ( errPoolClosed = errors.New("gorethink: pool is closed") ) @@ -139,25 +137,16 @@ func (p *Pool) SetMaxOpenConns(n int) { // Exec executes a query without waiting for any response. func (p *Pool) Exec(q Query) error { - var err error - - for i := 0; i < maxBadConnRetries; i++ { - var c *Connection - var pc *pool.PoolConn - - c, pc, err = p.conn() - if err != nil { - continue - } - defer pc.Close() + c, pc, err := p.conn() + if err != nil { + return err + } + defer pc.Close() - _, _, err = c.Query(q) + _, _, err = c.Query(q) - if c.isBad() { - pc.MarkUnusable() - } else { - break - } + if c.isBad() { + pc.MarkUnusable() } return err @@ -165,28 +154,17 @@ func (p *Pool) Exec(q Query) error { // Query executes a query and waits for the response func (p *Pool) Query(q Query) (*Cursor, error) { - var cursor *Cursor - var err error - - for i := 0; i < maxBadConnRetries; i++ { - var c *Connection - var pc *pool.PoolConn - - c, pc, err = p.conn() - if err != nil { - continue - } + c, pc, err := p.conn() + if err != nil { + return nil, err + } - _, cursor, err = c.Query(q) + _, cursor, err := c.Query(q) - if err == nil { - cursor.releaseConn = releaseConn(c, pc) - } else if c.isBad() { - pc.MarkUnusable() - continue - } - - break + if err == nil { + cursor.releaseConn = releaseConn(c, pc) + } else if c.isBad() { + pc.MarkUnusable() } return cursor, err @@ -195,25 +173,17 @@ func (p *Pool) Query(q Query) (*Cursor, error) { // Server returns the server name and server UUID being used by a connection. func (p *Pool) Server() (ServerResponse, error) { var response ServerResponse - var err error - - for i := 0; i < maxBadConnRetries; i++ { - var c *Connection - var pc *pool.PoolConn - c, pc, err = p.conn() - if err != nil { - continue - } - defer pc.Close() + c, pc, err := p.conn() + if err != nil { + return response, err + } + defer pc.Close() - response, err = c.Server() + response, err = c.Server() - if c.isBad() { - pc.MarkUnusable() - } else { - break - } + if c.isBad() { + pc.MarkUnusable() } return response, err diff --git a/session.go b/session.go index 63ab60d8..e820ca17 100644 --- a/session.go +++ b/session.go @@ -59,6 +59,11 @@ type ConnectOpts struct { // Indicates whether the cursors running in this session should use json.Number instead of float64 while // unmarshaling documents with interface{}. The default is `false`. UseJSONNumber bool + + // NumRetries is the number of times a query is retried if a connection + // error is detected, queries are not retried if RethinkDB returns a + // runtime error. + NumRetries int } func (o *ConnectOpts) toMap() map[string]interface{} { diff --git a/utils.go b/utils.go index 03c1e2da..166b6063 100644 --- a/utils.go +++ b/utils.go @@ -266,3 +266,17 @@ func encode(data interface{}) (interface{}, error) { return v, nil } + +// shouldRetryQuery checks the result of a query and returns true if the query +// should be retried +func shouldRetryQuery(q Query, err error) bool { + if err == nil { + return false + } + + if _, ok := err.(RQLConnectionError); ok { + return true + } + + return err == ErrConnectionClosed +} From 504ee66dafa237a9367c911b9af3b70cd79d8202 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Wed, 25 May 2016 20:44:00 +0100 Subject: [PATCH 6/9] Exported the Build function --- CHANGELOG.md | 4 ++++ connection.go | 4 ++-- query.go | 10 +++++----- utils.go | 6 +++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0cefa7..4cc6ebd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). + Queries can be mocked using `On` and `Return`, `Mock` also contains functions for asserting that the required mocked queries were executed. + For more information about how to mock queries see the readme and tests in `mock_test.go`. +## Changed + +- Exported the `Build()` function on `Query` and `Term`. + ## v2.0.4 - 2016-05-22 ### Changed diff --git a/connection.go b/connection.go index 7e8505ac..e9eef3f7 100644 --- a/connection.go +++ b/connection.go @@ -124,7 +124,7 @@ func (c *Connection) Query(q Query) (*Response, *Cursor, error) { if q.Type == p.Query_START || q.Type == p.Query_NOREPLY_WAIT { if c.opts.Database != "" { var err error - q.Opts["db"], err = DB(c.opts.Database).build() + q.Opts["db"], err = DB(c.opts.Database).Build() if err != nil { c.mu.Unlock() return nil, nil, RQLDriverError{rqlError(err.Error())} @@ -190,7 +190,7 @@ func (c *Connection) Server() (ServerResponse, error) { // sendQuery marshals the Query and sends the JSON to the server. func (c *Connection) sendQuery(q Query) error { // Build query - b, err := json.Marshal(q.build()) + b, err := json.Marshal(q.Build()) if err != nil { return RQLDriverError{rqlError("Error building query")} } diff --git a/query.go b/query.go index 7a5bd115..975fc889 100644 --- a/query.go +++ b/query.go @@ -21,7 +21,7 @@ type Query struct { builtTerm interface{} } -func (q *Query) build() []interface{} { +func (q *Query) Build() []interface{} { res := []interface{}{int(q.Type)} if q.Term != nil { res = append(res, q.builtTerm) @@ -56,7 +56,7 @@ type Term struct { // build takes the query tree and prepares it to be sent as a JSON // expression -func (t Term) build() (interface{}, error) { +func (t Term) Build() (interface{}, error) { var err error if t.lastErr != nil { @@ -73,7 +73,7 @@ func (t Term) build() (interface{}, error) { case p.Term_MAKE_OBJ: res := map[string]interface{}{} for k, v := range t.optArgs { - res[k], err = v.build() + res[k], err = v.Build() if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (t Term) build() (interface{}, error) { optArgs := make(map[string]interface{}, len(t.optArgs)) for i, v := range t.args { - arg, err := v.build() + arg, err := v.Build() if err != nil { return nil, err } @@ -100,7 +100,7 @@ func (t Term) build() (interface{}, error) { } for k, v := range t.optArgs { - optArgs[k], err = v.build() + optArgs[k], err = v.Build() if err != nil { return nil, err } diff --git a/utils.go b/utils.go index 166b6063..bc160e32 100644 --- a/utils.go +++ b/utils.go @@ -43,19 +43,19 @@ func constructMethodTerm(prevVal Term, name string, termType p.Term_TermType, ar func newQuery(t Term, qopts map[string]interface{}, copts *ConnectOpts) (q Query, err error) { queryOpts := map[string]interface{}{} for k, v := range qopts { - queryOpts[k], err = Expr(v).build() + queryOpts[k], err = Expr(v).Build() if err != nil { return } } if copts.Database != "" { - queryOpts["db"], err = DB(copts.Database).build() + queryOpts["db"], err = DB(copts.Database).Build() if err != nil { return } } - builtTerm, err := t.build() + builtTerm, err := t.Build() if err != nil { return q, err } From 65ff12194aa78d561a988779131eeb8c38fb660b Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sun, 26 Jun 2016 19:18:22 +0100 Subject: [PATCH 7/9] Bumped version number --- CHANGELOG.md | 2 +- README.md | 2 +- doc.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc6ebd4..dafa31c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## v2.1.0 - 2016-06-26 ### Added diff --git a/README.md b/README.md index 4658d4fe..352769be 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![GoRethink Logo](https://raw.github.com/wiki/dancannon/gorethink/gopher-and-thinker-s.png "Golang Gopher and RethinkDB Thinker") -Current version: v2.0.4 (RethinkDB v2.3) +Current version: v2.1.0 (RethinkDB v2.3) Please note that this version of the driver only supports versions of RethinkDB using the v0.4 protocol (any versions of the driver older than RethinkDB 2.0 will not work). diff --git a/doc.go b/doc.go index 4d7124de..df565cfa 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ // Package gorethink implements a Go driver for RethinkDB // -// Current version: v2.0.4 (RethinkDB v2.3) +// Current version: v2.1.0 (RethinkDB v2.3) // For more in depth information on how to use RethinkDB check out the API docs // at http://rethinkdb.com/api package gorethink From 47b3745fcdf0cc9f8efd02c3199e8ef89106bd03 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sun, 26 Jun 2016 19:35:56 +0100 Subject: [PATCH 8/9] Fixed build --- mock.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mock.go b/mock.go index a486e4e6..7611e569 100644 --- a/mock.go +++ b/mock.go @@ -48,7 +48,7 @@ type MockQuery struct { func newMockQuery(parent *Mock, q Query) *MockQuery { // Build and marshal term - builtQuery, err := json.Marshal(q.build()) + builtQuery, err := json.Marshal(q.Build()) if err != nil { panic(fmt.Sprintf("Failed to build query: %s", err)) } @@ -329,7 +329,7 @@ func (m *Mock) newQuery(t Term, opts map[string]interface{}) (Query, error) { func (m *Mock) findExpectedQuery(q Query) (int, *MockQuery) { // Build and marshal query - builtQuery, err := json.Marshal(q.build()) + builtQuery, err := json.Marshal(q.Build()) if err != nil { panic(fmt.Sprintf("Failed to build query: %s", err)) } From c8df9e37dbc5e4e808e7953480a405d71cc94d0d Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sun, 26 Jun 2016 19:40:19 +0100 Subject: [PATCH 9/9] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dafa31c5..512c4596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Changed - Exported the `Build()` function on `Query` and `Term`. +- Updated import of `github.com/cenkalti/backoff` to `github.com/cenk/backoff` ## v2.0.4 - 2016-05-22