Smock — An Optimistic Adventure in Wonderland

DeFi Wonderland
5 min readAug 11, 2021

--

As many of you know, Smock is a mighty tool for mocking solidity contracts developed by Kelvin Fichter, one of the mad people behind Optimistic Ethereum (Optimism).

At DeFi Wonderland we have long ago decided to partake in this adventure of making Solidity Testing as easy and powerful as it can get, that’s why we’ve partnered with Optimism to develop Smock 2.0 together.

So… what does the new smock have to offer?

smock makes the process of testing complex smart contracts significantly easier. You’ll never have to write another mock contract in Solidity again.

  • Get rid of your folder of “mock” contracts and just use JavaScript.
  • Keep your tests simple with a sweet set of chai matchers.
  • Fully compatible with TypeScript and TypeChain.
  • Manipulate the behavior of functions on the fly with fakes.
  • Modify functions and internal variables of a real contract with mocks.
  • Make assertions about calls, call arguments, and call counts.
  • We’ve got extensive documentation and a complete test suite.

Why do we test?

If you are into Ethereum, then you probably know that contracts are immutable, and that developers must spend most of their time testing their own code. If a developer, by some intricated coding or few hours of sleep, broke some functionality of his contract, he needs to be warned somehow. That’s what tests are made for.

So what’s a mock contract?

Let’s say we have the following contract, not so different from the common Greeter that you’ll find all around Solidity tutorials, with the only difference that will come handy for this explanation: after the 10th greet change, it will change it no more.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
contract LazyGreeter { string public greeting = ‘Hello World!’;
int24 public count;
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public returns (bool _changedGreet) {
if (count < 10) {
greeting = _greeting;
_changedGreet = true;
count += 1;
}
}

As you can see, there’s a setter function for the greet, but there’s not a setter for the count. But the count is easily predictable, if you call the setter function, the count will get +1, until it reaches 10.

So performing a test on this SUPER simple feature should be quite easy: we just call the contract setGreeting function 11 times and see what happens! That’s called an end-to-end test, we interact with the contract as we would on the EVM, just calling it’s functions, and we may be able to enjoy some of the advantages of running this contract in our own local node (like having A LOT of ETH to play with, gas free transactions, or being able to fake other people’s signatures, just for the sake of testing).

In an end-to-end test we have several disadvantages, to see what happens after greet change n10, we should need to make 10 transactions. So if any of those transactions would fail before we’ve got to the sweet spot, we are going to have a hard time trying to debug it (because the test was not supposed to fail in the way it failed). And what if the limit for the counter was more than 10? Let’s say… 10¹⁰ (number completely possible). So then we would need to make 10000000001 transactions to get a glimpse of our precious feature. That can take AGES.

So, as with any other problem in life, there’s always multiple approaches to it:

Approach A “The just call it 10 times”

   greeterFactory = (await ethers.getContractFactory('LazyGreeter')) as LazyGreeterForTest__factory;
greeter = await greeterFactory.deploy('Hello, world!');
[...]
it("Should not change the greeting more than 10 times", async () =>{
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
await greeter.setGreeting('Hola, mundo!');
expect(await greeter.callStatic.setGreeting('Hola, mundo!')).to.be.false;
})
});

Notice that our tests are Typified, that’s a really helpful interaction between the IDE and our contracts, predicting us which functions are available to use, or not even starting to run the test if something is just not right. If you like to test this typifying, I’d suggest you take a look at our Solidity Boilerplate.

Approach B “The use another, but similar, contract”

So this is where a unit test comes handy. In unit tests we give ourselves the permission to modify a little bit our contract, to be able to focus on that particular function, and not the whole environment around it. So up until Aug-2021, this is what we were supposed to do: Mock Contracts.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
import './LazyGreeter.sol';contract LazyGreeterForTest is LazyGreeter {
constructor(string memory _greeting) LazyGreeter(_greeting) {}

function setCounter(uint256 _count) external {
count = _count;
}
}

So now we have a different contract, it’s called LazyGreeterForTest, it imports LazyGreeter (with its variables and functions), and adds a piece of code that is definitely NOT going to be in the deployed contract: a setter for the count!

greeterForTestFactory = (await ethers.getContractFactory('LazyGreeterForTest')
greeterForTest = await greeterFactory.deploy('Hello, world!');
[...]
it('Should not change the greeting more than 10 times', async () => {
await greeterForTest.setCounter(10);
expect(await greeterForTest.callStatic.setGreeting('Hola, mundo!')).to.be.false;
});
});

As you can see, it’s fairly easy to recognize that the contract is quite the same, so the feature we’re looking after should behave the same as well. But when contracts start to get rather Messi, we can end up having more ForTest contracts than actual contracts!

So this is where Smock comes into play.

Approach C “The just Smock it!”

     const greeterFactory = await smock.mock<LazyGreeter__factory>('LazyGreeter');
greeter = await greeterFactory.deploy('Hello, world!');
[...]
it('Should not change the greeting more than 10 times', async () => {
greeter.setVariable('count',10);
expect(await greeter.callStatic.setGreeting('Hola, mundo!')).to.be.false;
});
});

Notice that the setVariable is a functionality of Smock, and not the contract!

Advantages:

  • One does not need to test his contract through another similar one
  • One does not need to test the ForTest contracts (Smock is fully tested)
  • One does not need to code extra .sol files
  • One could program a whole function response instead of it’s internal variables
  • One could program a different response for each call
  • One could assert that a certain function has/hasn’t been called in the interaction

And this is just the tip of the iceberg, go check out the docs!

https://smock.readthedocs.io/en/latest/

--

--