-
Notifications
You must be signed in to change notification settings - Fork 334
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
meta: how to improve mocking in forge-std? #46
Comments
One thing I did not realize it that people need out of gas simulation. I just created this issue in my mocking provider repo. I will keep following this thread and I will try to contribute. |
Another feature I'd like a lot, but probably needs to be done in foundry, is the ability to capture values and/or do better assertions on verify. Right now, the only way to check that a dependency was called is to do vm.expectCall(address(dep), abi.encodeWithSelector(dep.foo.selector, param1, paramN));
dep.foo(); There are a couple of issues with this approach
So what I'd love to have is something like this dep.foo();
bytes[] memory captures = vm.capure(address(dep), dep.foo.selector);
assertEq(captures.length, 1, "Method wasn't called the expected number of times!");
(int param1, address param2, ... , paramX) = abi.decode(captures[0], (int, address, ...);
assertGt(param1, 0); // Can do non-exact match of the param
// Can call something else on param2 now that I know it's address
// etc In a way it's kinda similar to the existing Thoughts? |
@bbonanno You should have a closer look at my library because, as I understand, it already supports the features you're requesting. For example you can mock a method, without the need to specify the exact parameters. As long as the An example: // Make it return false whenever calling .isEven(anything)
provider.givenSelectorReturnResponse(
// Respond to `.isEven()`
abi.encodePacked(IOddEven.isEven.selector),
// Encode the response
MockProvider.ReturnData({
success: true,
data: abi.encodePacked(bool(false))
}),
// Log the event
false
); That means that you could call the mocked method provider.isEven(1) == false
provider.isEven(42) == false Another way to achieve even more flexibility is to set a default response for ANY kind of request. Code example: // Make the provider successfully respond with 42 for any request
provider.setDefaultResponse(
MockProvider.ReturnData({
success: true,
data: abi.encode(uint256(42))
})
); If you want to inspect the requests being made you can even enable logging and check the logged calls afterward. provider.getCallData(0); // 0 is the index of the call, it logs all requests. A caveat to enabling default responses is if there is any call that uses the opcode |
@cleanunicorn the partial mocking is also possible with the standard mockCall, that's not an issue. For the capturing, looking at the logs is what I do now, but is not ideal, and it doesn't solve the issue of not doing exact comparison with the values. I'm trying to do something like an ArgumentCaptor in mockito (if you ever used it) |
I spent some time trying to understand Mockito, but I am not a Java developer but the idiosyncratic approach doesn't make a lot of sense to me. This is the article I spent most time with https://www.baeldung.com/mockito-argumentcaptor And I did not understand the advantages of using Mockito vs normal mocking. Going back to your "ideal" code example, it still doesn't make a lot of sense. I guess this is because you didn't have enough time to explain how everything should work. I am going to split that into multiple sections and try to explain what I understand and ask additional questions. dep.foo(); I assume this just calls the method that you want to mock or test. Unfortunately, it's not what what is mocked and what is tested, what is the implementation being tested and what is the mocked code connected to the implementation. bytes[] memory captures = vm.capure(address(dep), dep.foo.selector); I assume this captures the previous calls to the assertEq(captures.length, 1, "Method wasn't called the expected number of times!"); This checks how many calls were made to the method identified by (int param1, address param2, ... , paramX) = abi.decode(captures[0], (int, address, ...); This takes the first logged call and decodes something. It's not clear what it decodes because the method assertGt(param1, 0); // Can do non-exact match of the param
// Can call something else on param2 now that I know it's address
// etc This check makes sense to me, I understand the non-exact match you want to do. Coming back to my mocker, it seems this is possible. Here is a snippet that mocks a contract that is expecting a call from Drop this in Remix and play with it. import "https://github.com/cleanunicorn/mockprovider/blob/master/src/MockProvider.sol";
contract Implementation {
address callInto;
constructor(address callInto_) {
callInto = callInto_;
}
function foo() public {
callInto.call(
abi.encodeWithSelector(0x11223344, 1, 2, 3, 4)
);
}
}
contract TestImplementation {
function test_foo() public {
MockProvider provider = new MockProvider();
provider.setDefaultResponse(MockProvider.ReturnData({
success: true,
data: ""
}));
Implementation implementation = new Implementation(provider);
implementation.foo();
MockProvider.ReturnData memory rd = provider.getCallData(0);
(uint a, address b, uint c, uint d) = abi.decode(rd.data, (uint, address, uint, uint));
// You can check each item individually
// a > 0
// b == 2
// c not even
// ...
}
} You can test each item (a, b, c, d) individually with your own rules. |
Yeah, now I realised my example is a bit shit, but you totally nailed it, that's exactly what I'm talking about. Having a look at the code and looks cool, my noob self didn't know that For your first question, given the way the Java compiler and the JVM works, mockito allows you to create mocks without having to write specific classes for each one, pretty much what your lib does, albeit is waaaay simpler given the tools (or lack of) that solidity has. I think your lib is an amazing starting point, but I also think is missing a few features, I'm all up for adding a few PRs if you like :) Also not sure if the path is to merge that with forge-std or to keep it as a separate lib Thoughts? |
I think it's great that you want to contribute. I am not sure if the current interaction API format should be simplified. In terms of being merged with forge-std, currently I am not sure. The ability to mock is something really useful. If I start adding forge-specific features it could be merged, bit not required to. Otherwise it can be framework agnostic. |
Thanks for the discussion @bbonanno and @cleanunicorn! 🙂 Here's one more mocking library, by @PARABRAHMAN0 (h/t @gakonst in the foundry telegram) Would love to also get some thoughts from @maurelian and @PaulRBerg. Either way, I'll try to share my own thoughts / propose a path forward tomorrow in the next few days |
yeah the UX with this mocking library is quite nice: https://github.com/OlympusDAO/test-utils/blob/master/src/test/mocking.t.sol#L36-L61 |
Nice, I didn't know Solidity had higher order functions! |
I've just caught up on everything that has been discussed here. My thoughts:
In terms of what mock contract to choose, my votes go to @cleanunicorn's
|
That said I don't plan to maintain it, so anyone is welcome to copy the code into a new repo and run with it if they wish. |
Awesome, yea I tend to agree with the last two responses in that:
Admittedly I'm not the biggest user of mocks in forge so I'm not the best person to answer this, but here's my next two questions:
|
Just an update here that as of foundry-rs/foundry#2576, foundry now automatically etches a single byte to empty accounts as part of |
Does it make sense to add a |
I like it, it'd be the equivalent of mocking an answer, so I guess the param would be a function type reference to a public function in the same test? (not sure if the compiler would like that though) |
Sorry do you mind clarifying? Not sure I totally follow the idea here. Maybe an example would help. |
Yeah... Something like this: contract MyTest is Test {
// This would have the same i/o signature as the mocked function.
function myMockFunction(address foo) public returns(string memory) {
return "hello world!";
}
function testSomething() public {
// Redirect calls to `123.foo` to `this.myMockFunction`
vm.mockCall(address(123), "foo(address)", address(this), this.myMockFunction.selector);
...
}
} |
Ah I see. Though I don't follow see the use case for it / when would you need to do that? FWIW you can also |
I had a case just now in an integration test where it was rather difficult to figure out exactly what values the function I wanted to mock would be called with and I wanted to return something based on the input. So yeah, add some minimal amount of dynamic logic to the test. I generally try to avoid that and normally would consider that a bad idea but it was hard to get around in this instance.
|
Seems like a niche use case so I'm not sure it's worth a cheatcode, but an issue in the foundry repo would be the right place to discuss that.
Yep, so you can take the original source, modify it, then compile and use |
Hey @mds1 ! (always fun to see you around here :) Let's say I mock a specific contract/function and want to test that it was called |
Hey! I'm not too familiar with how jest mocks work, but from that description I don't think it's currently possible, though there is a feature request for it: foundry-rs/foundry#4513. |
Going to close this now that we have a |
Forge has two cheatcodes for mocking:
This results in the following mocking UX native to forge:
mockCall
etch
ing dummy code to that addressThis is ok, but not great. Given that, what should forge-std do to improve mocking UX, and what should be upstreamed to forge?
For context, various mocking contracts exist to help with the above scenarios (note that I have not carefully reviewed each, these are just my quick impressions):
MockContract.sol
is very flexible, and allows you to mock reverts and out of gas errorsDoppelganger.sol
is similar. It has less code (so presumably faster to compile) but does not support mocking out of gas errors.UniMock
seems to be the smallest, and could probably be easily extended to mock an out of gas error.MockPrivder
seems like another good option.My initial reaction is:
mockCall
so it can support mocking reverts with specified data. (Maybe support mocking out of gas errors too? Not sure how common needing to mock OOG is, just mentioning it since gnosis's mock supports it).mockCall
as the default for when you need to mock responses on contracts that already exist.deployMock()
anddeployMock(address where)
helper methods. This would be used in cases where you need a mock at an address that doesn't have any code.I think this should should cover all popular use cases of mocking, and uncommon use cases probably don't need to be supported in forge-std. But let me know if there's common scenarios I missed!
Tagging @bbonanno @maurelian and @cleanunicorn since I think you all may have some thoughts/feedback on the above 🙂
The text was updated successfully, but these errors were encountered: