diff --git a/go.mod b/go.mod index 85f8be6..2a7aa4c 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.23 require ( github.com/spaolacci/murmur3 v1.1.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3b0db7c..c4aebae 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/locator.go b/locator.go index 7413673..c350a38 100644 --- a/locator.go +++ b/locator.go @@ -10,7 +10,7 @@ import ( var ( // ErrNoServices is returned by a Locator to indicate that the Locator contains // no service entries. - ErrNoServices = errors.New("no services defined in this locator") + ErrNoServices = errors.New("no services defined") ) // Locator is a service locator based on hashing input objects. diff --git a/locator_test.go b/locator_test.go new file mode 100644 index 0000000..9c48120 --- /dev/null +++ b/locator_test.go @@ -0,0 +1,224 @@ +package medley + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type LocatorSuite struct { + suite.Suite + + object []byte + objectString string +} + +func (suite *LocatorSuite) find(ml *MultiLocator[string]) ([]string, error) { + return ml.Find(suite.object) +} + +func (suite *LocatorSuite) findString(ml *MultiLocator[string]) ([]string, error) { + return ml.FindString(suite.objectString) +} + +func (suite *LocatorSuite) SetupTest() { + suite.objectString = "test value" + suite.object = []byte(suite.objectString) +} + +func (suite *LocatorSuite) assertExpectations(testObjects ...any) bool { + return mock.AssertExpectationsForObjects( + suite.T(), + testObjects..., + ) +} + +func (suite *LocatorSuite) TestFindString() { + l := new(MockLocator[string]) + l.ExpectFindSuccess(suite.object, "service1").Once() + + actual, err := FindString(l, suite.objectString) + suite.NoError(err) + suite.Equal("service1", actual) + + suite.assertExpectations(l) +} + +func (suite *LocatorSuite) testMultiLocatorFindEmpty() { + ml := new(MultiLocator[string]) + results, err := ml.Find(suite.object) + suite.ErrorIs(err, ErrNoServices) + suite.Empty(results) +} + +func (suite *LocatorSuite) testMultiLocatorFindStringEmpty() { + ml := new(MultiLocator[string]) + results, err := ml.FindString(suite.objectString) + suite.ErrorIs(err, ErrNoServices) + suite.Empty(results) +} + +// testMultiLocatorAllSuccess sets the expectation for a suite.object call on contained +// locators and lets a test pass in a closure to invoke a method on the MultiLocator under test. +func (suite *LocatorSuite) testMultiLocatorAllSuccess(finder func(*MultiLocator[string]) ([]string, error)) func() { + return func() { + var ( + l1 = new(MockLocator[string]) + l2 = new(MockLocator[string]) + l3 = new(MockLocator[string]) + + ml = NewMultiLocator(l1, l2) + ) + + l1.ExpectFindSuccess(suite.object, "service1").Times(2) + l2.ExpectFindSuccess(suite.object, "service2").Times(3) + l3.ExpectFindSuccess(suite.object, "service3").Times(2) + + results, err := finder(ml) + suite.NoError(err) + suite.ElementsMatch([]string{"service1", "service2"}, results) + + ml.Add(l3) + results, err = finder(ml) + suite.NoError(err) + suite.ElementsMatch([]string{"service1", "service2", "service3"}, results) + + ml.Remove(l1) + results, err = finder(ml) + suite.NoError(err) + suite.ElementsMatch([]string{"service2", "service3"}, results) + + suite.assertExpectations(l1, l2, l3) + } +} + +// testMultiLocatorSomeMissingServices tests that FindXXX works correctly when some locators +// are missing services, but others aren't. +func (suite *LocatorSuite) testMultiLocatorSomeMissingServices(finder func(*MultiLocator[string]) ([]string, error)) func() { + return func() { + var ( + l1 = new(MockLocator[string]) + l2 = new(MockLocator[string]) + l3 = new(MockLocator[string]) + + ml = NewMultiLocator(l1, l2, l3) + ) + + l1.ExpectFindSuccess(suite.object, "service1").Once() + l2.ExpectFindNoServices(suite.object).Once() + l3.ExpectFindSuccess(suite.object, "service3").Once() + + results, err := finder(ml) + suite.NoError(err) + suite.ElementsMatch([]string{"service1", "service3"}, results) + + suite.assertExpectations(l1, l2, l3) + } +} + +// testMultiLocatorFail tests that FindXXX works correctly when a locator returns an error. +func (suite *LocatorSuite) testMultiLocatorFail(finder func(*MultiLocator[string]) ([]string, error)) func() { + return func() { + var ( + expectedErr = errors.New("expected") + + l1 = new(MockLocator[string]) + l2 = new(MockLocator[string]) + l3 = new(MockLocator[string]) + + ml = NewMultiLocator(l1, l2, l3) + ) + + l1.ExpectFindSuccess(suite.object, "service1").Once() + l2.ExpectFindFail(suite.object, expectedErr).Once() + + results, err := finder(ml) + suite.ErrorIs(err, expectedErr) + suite.Empty(results) + + suite.assertExpectations(l1, l2, l3) + } +} + +func (suite *LocatorSuite) TestMultiLocator() { + suite.Run("Find", func() { + suite.Run("Empty", suite.testMultiLocatorFindEmpty) + suite.Run("AllSuccess", suite.testMultiLocatorAllSuccess(suite.find)) + suite.Run("SomeMissingServices", suite.testMultiLocatorSomeMissingServices(suite.find)) + suite.Run("Fail", suite.testMultiLocatorFail(suite.find)) + }) + + suite.Run("FindString", func() { + suite.Run("Empty", suite.testMultiLocatorFindStringEmpty) + suite.Run("AllSuccess", suite.testMultiLocatorAllSuccess(suite.findString)) + suite.Run("SomeMissingServices", suite.testMultiLocatorSomeMissingServices(suite.findString)) + suite.Run("Fail", suite.testMultiLocatorFail(suite.findString)) + }) +} + +func (suite *LocatorSuite) TestUpdatableLocator() { + var ( + expectedErr = errors.New("expected error") + + l1 = new(MockLocator[string]) + l2 = new(MockLocator[string]) + l3 = new(MockLocator[string]) + + ul = NewUpdatableLocator(l1) + ) + + suite.Require().NotNil(ul) + + l1.ExpectFindSuccess(suite.object, "service1").Once() + l2.ExpectFindSuccess(suite.object, "service2").Once() + l3.ExpectFindFail(suite.object, expectedErr).Once() + + result, err := ul.Find(suite.object) + suite.NoError(err) + suite.Equal("service1", result) + + ul.Set(nil) + result, err = ul.Find(suite.object) + suite.ErrorIs(err, ErrNoServices) + suite.Empty(result) + + ul.Set(l2) + result, err = ul.Find(suite.object) + suite.NoError(err) + suite.Equal("service2", result) + + ul.Set(l3) + result, err = ul.Find(suite.object) + suite.ErrorIs(err, expectedErr) + suite.Empty(result) + + suite.assertExpectations(l1, l2, l3) +} + +func (suite *LocatorSuite) TestSetLocator() { + var ( + l1 = new(MockLocator[string]) + l2 = new(MockLocator[string]) + ul = NewUpdatableLocator[string](nil) + ) + + l2.ExpectFindSuccess(suite.object, "service2").Once() + suite.Require().NotNil(ul) + + suite.NotPanics(func() { + SetLocator(l1, l2) // nop, since l1 doesn't have a Set method + }) + + SetLocator(ul, l2) + result, err := ul.Find(suite.object) + suite.NoError(err) + suite.Equal("service2", result) + + suite.assertExpectations(l1, l2) +} + +func TestLocator(t *testing.T) { + suite.Run(t, new(LocatorSuite)) +} diff --git a/mocks_test.go b/mocks_test.go new file mode 100644 index 0000000..4690686 --- /dev/null +++ b/mocks_test.go @@ -0,0 +1,27 @@ +package medley + +import "github.com/stretchr/testify/mock" + +type MockLocator[S Service] struct { + mock.Mock +} + +func (m *MockLocator[S]) Find(object []byte) (S, error) { + args := m.Called(object) + + svc, _ := args.Get(0).(S) + return svc, args.Error(1) +} + +func (m *MockLocator[S]) ExpectFindSuccess(object any, result S) *mock.Call { + return m.On("Find", object).Return(result, error(nil)) +} + +func (m *MockLocator[S]) ExpectFindFail(object any, err error) *mock.Call { + var zero S + return m.On("Find", object).Return(zero, err) +} + +func (m *MockLocator[S]) ExpectFindNoServices(object any) *mock.Call { + return m.ExpectFindFail(object, ErrNoServices) +}