goto / guardian

Guardian is a tool for extensible and universal data access with automated access workflows and security controls across data stores, analytical systems, and cloud products.

Home Page:https://goto.github.io/guardian/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Make mocks independent at subtest level

rahmatrhd opened this issue · comments

Summary
Some of our tests use shared mock instances across subtests within a test suite. This could cause expectations set from subtest A still leave open in subtest B if it's not fulfilled in subtest A. This is due to a limitation from testify that before/after test hook is not available for subtest. Currently, we avoid that issue by ensuring the number of expectations matches the actual executions using .Once(). But a similar issue still could happen in parallel tests.

The second issue is in a complex function that makes many calls from dependencies, we had to set the expectations for all executed calls in each subtest resulting in the code being not DRY and making the test harder to read/understand.

Proposed solution
In testify v1.8.2, before and after test hooks are now supported for subtests. We can use this to ensure every subtest starts with empty expectations.

To also fix the second issue as well as for the parallel testing, mocks need to be decoupled from the suite struct and can be initialized at anytime when needed.

Proposing following approach:

package appeal_test

type mock struct {
	mockRepository *appeal.Repository
	mockXService   *appeal.XService

	service *appeal.Service
}

func newMock() mock {
	m := mock{
		mockRepository: new(mockRepository),
		mockXService:   new(mockXService),
	}
	m.service = appeal.NewService(m.mockRepository, m.mockXService)
	return m
}

func (m *mock) setSuccessExpectations() {
	// always expect mock.Anything in params and return success values
	m.mockRepository.EXPECT().Create(mock.Anything, mock.Anything).Return(nil)
	m.mockXService.EXPECT().Create(mock.Anything, mock.Anything).Return(nil)
}

type AppealTestSuite struct {
	suite.Suite
	mock // shared mock
}

func (s *AppealTestSuite) SetupSubTest() {
	s.mock = newMock() // reinitialize mock for each sub test
}

// usage example
func (s *AppealTestSuite) TestCreate() {
	s.Run("repository returns an error", func() {
		s.mockRepository.EXPECT().Create(mock.Anything, mock.Anything).Return(errors.New("error"))
		_, err := s.service.Create(context.Background(), appeal.CreateRequest{})
		s.Error(err)
	})

	s.Run("test specific case", func() {
		s.setSuccessExpectations() // set common expectations
		
		// only specify expectations related to the tested case:
		s.mockXService.EXPECT().Create(mock.Anything, mock.Anything).Return(errors.New("error"))

		_, err := s.service.Create(context.Background(), appeal.CreateRequest{})
		s.Error(err)
	})

	s.Run("parallel tests", func() {
		testCases := []struct {}{
			// ...
		}

		for _, tc := range testCases {
			tc := tc
			s.Run(tc.name, func() {
				s.T().Parallel()

				independentMock := newMock()
				// set expectations for independentMock
				// ...
				//_, err := independentMock.service.Create(context.Background(), appeal.CreateRequest{})
			})
		}
	}
}
  1. decoupled mocks from suite struct into an independent struct
  2. restart mocks in every subtests s.mock = newMock()
  3. for parallel tests, use an independent mock instead of a shared one
  4. introduced setSuccessExpectations() to avoid writing common expectations multiple times across subtests

Note: currently trying out this approach for appeal service as a POC, will raise the PR sson

func (m *mock) setSuccessExpectations() {
	// always expect mock.Anything in params and return success values
	m.mockRepository.EXPECT().Create(mock.Anything, mock.Anything).Return(nil)
	m.mockXService.EXPECT().Create(mock.Anything, mock.Anything).Return(nil)
}

I don't think we should have something like this which always expects only mock.Anything. We can try to abstract common expectations, but that should not come at a cost of proper expectation checks.

I understand that we do use mock.Anything pervasively in our code, but that is something that should change rather than be embraced.