-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP][Testing] Mock Persistence Module in Utility Tests - An Example #290
Conversation
utility/test/experimental_test.go
Outdated
require.Equal(t, []byte(" 1 getHash 42 getHash "), appHash) | ||
} | ||
|
||
func newMockablePersistenceContextForAppHashTestWithMoreState(t *testing.T, height int64) modules.PersistenceRWContext { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if I'm understanding correctly, everytime we have a unit test we need a separate function to mock that functionality - like a 1:1 mapping of sorts?
Is there any overlapping Mock functionality we may genericize/share? If so, can you demonstrate that in an example?
EDIT:
(The above assumes we go full Mock approach and not use the 'real' persistence module for testing)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conversation around 'full mocking' vs 'hybrid' here #290 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like a 1:1 mapping of sorts?
Doesn't have to be a 1:1 mapping.
You can create helpers and then reuse them:
Imagine a common_mocks_test.go
file
func sharedAddAccountAmount(address []byte, amount string) error {
... // implementation
}
// define other common functions
sharedPersistenceContextMock := initializedPersistenceMock()
sharedPersistenceContextMock.EXPECT().
AddAccountAmount(gomock.Any(), gomock.Any()).
DoAndReturn(sharedAddAccountAmount).
AnyTimes()
// expect other common functions
some_test.go
persistenceContextMock = copy(sharedPersistenceContextMock)
// override some subset of mocks specific to this
some_other_test.go
persistenceContextMock = copy(sharedPersistenceContextMock)
// override some other subset of mocks specific to this
utility/test/experimental_test.go
Outdated
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// This test just tries to add amounts to an account and check the app hash via the real implementation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this seems relatively elegant. I think we some proper common helper funcs we might be able to have a decent testing suite
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, the way I've seen this done before is that you don't build it out all at once, but you add the functions/components you need over time.
E.g. If you need to mock AddAccountAmount
now, we add it alongside the new logic, and keep appending in the future.
This appears to not satisfy the encapsulation requirement, why don't we just Mock everything instead of using the persistence module? It appears (atleast for the unit tests) we would be able to use the |
Yea, I see it as a step in the right direction.
I guess I have varying viewpoints here but don't feel strongly.
Correct. tl;dr I think what the "passthrough" approach does is unblock work that's dependant on functionality we don't have yet, without needing to pay the upfront cost of doing a complete second implementation of the persistence module. This may or may not be a good idea, but I think we will have visibility into it as we start mocking parts of it and get a clearer picture. |
@Olshansk seems like a decent amount of techdebt will be created if we go passthrough approach. I caution about the snowplow effect that @jessicadaugherty alluded to. The current open M3 issues for this sprint are not blocked by this, but the future ones will be. Since this is an XL task, I'd encourage us to go all the way with the Mock |
To make sure I understand, are you suggesting to maintain two "feature complete" versions of the persistence module, or just mocking everything in the persistence module interface with hardcoded values. Will ping to sync offline as well. |
I'm suggesting to mock the entire persistence module in the Utility module to fully encapsulate rather than importing the persistence module and passing through |
Codecov ReportBase: 0.00% // Head: 41.41% // Increases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #290 +/- ##
=========================================
+ Coverage 0 41.41% +41.41%
=========================================
Files 0 28 +28
Lines 0 2113 +2113
=========================================
+ Hits 0 875 +875
- Misses 0 1184 +1184
- Partials 0 54 +54
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
I have been thinking about this and I have a blurred idea that I wanted to share with you guys. How about a function that constructs a mock and then multiple methods that in a fluent way allow us to configure expected behaviours? Pseudo-code of a random scenario: persistenceContextMock:=newPersistenceContextMock(ctrl).
FromRuntimeMgr(runtimeMgr).
ShouldFailWhenGetAllFishermenIsCalledAtHeight(2, fmt.Errorf("some expected failure here").
AppHashShouldReturnAtHeight(2, []byte("somehash"))
persistenceMock:=newPersistenceModuleMock(ctrl).
WithPersistenceContextMock(persistenceContextMock).
SomeOtherBehaviouralMethod(...some_arguments) And these can also be wrapped in some sort of util if we call them all the time and change only a bunch of parameters. so in persistenceModuleMock := getHappyPathXXPersistenceModule() It might be more complex than it sounds especially because probably there's gonna be some state to be handled inside the mock itself 🤔 but we could leverage the built-in controller to keep track of calls instead. I need to read more in depth how it all works. |
Heads up: I added the typical Olshansky whitespaces in @deblasis I believe the implementation I pushed is a starting point to what you described so PTAL. @andrewnguyen22 I removed the code for my initial passthrough attempt and updated the unit tests in Note how in places where we have dependencies on accounts, I do: runTimeMgr := persistenceRuntimeMgr(t)
acc := GetAllTestingAccounts(t, ctx)[0]
ctx := NewTestUtilityContext(t, 0, withBaseAccountMock(t, runTimeMgr)) And in some places where we have dependencies on pools, I do: runTimeMgr := persistenceRuntimeMgr(t)
pool := GetAllTestingPools(t, ctx)[0]
ctx := NewTestUtilityContext(t, 0, withBasePoolMock(t, runTimeMgr)) What the current implementation achieves:
Please note that I do not believe this is "simple" or "low cognitive load" but fits the requirements of isolation by mocking the Let me know if jumping on a call would help instead. |
@andrewnguyen22 Wanted to bump this for you to provide some feedback if this is what you had in mind. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DUP COMMENT SEE BELOW
persistenceContextMock.EXPECT().Release().Return(nil).AnyTimes() | ||
|
||
// Adding behavioural mocks based on the options provided | ||
for _, o := range options { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without looking at the example, I'm not sure what options
does here.
Guess:
- You can add behaviors dynamically to the mock by sending wrapper functions through the parameter.
I think a detailed/clarifying comment above func NewTestUtilityContext(t *testing.T, height int64, options
would be helpful
return | ||
} | ||
|
||
mock.EXPECT().GetAllAccounts(gomock.Any()).DoAndReturn(func(height int64) ([]modules.Account, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think breaking these EXPECT()
s out in separate functions or leaving comments to divide them up would help reduce the cognitive load
@@ -96,7 +96,7 @@ func CleanupPostgresDocker(_ *testing.M, pool *dockertest.Pool, resource *docker | |||
os.Exit(0) | |||
} | |||
|
|||
// TODO(drewsky): Remove this in favor of a golang specific solution | |||
// TODO: Remove this in favor of a golang specific solution for after test teardown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be part of test teardown
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like this pattern!
It fits great in modularizing our software.
I left a few comments to help clarify the code a bit more and help with cognitive load.
I think this PR can be improved by doing an actor
specific mock as well.
P.S.
I noticed you @ me but didn't request a review. The best way to cut through the noise to get to me is to just request a review
Closing out this PR for now, but we can use it as a reference for future work on #261. |
Description
This PR is a WIP and is meant to be a starting point with an example.
The goal is to enable the utility module to be developed while dependencies in the persistence module are not yet ready. This paradigm could extend to any other cross-module interaction.
The comments contain more detail, but in short:
Issue
Fixes #261
Type of change
Please mark the relevant option(s):
TODO: List of changes
Testing
go test -v -count=1 -v ./utility/... -run TestExperimental
make develop_test
README
Required Checklist
If Applicable Checklist
shared/docs/*
if I updatedshared/*
README(s)