Design Pattern Solidity: Mock contracts for testing

Why you should make fun of your contracts

Mock objects are a common design pattern in object-oriented programming. Coming from the old French word 'mocquer' with the meaning of 'making fun of', it evolved to 'imitating something real' which is actually what we are doing in programming. Please only make fun of your smart contracts if you want to, but mock them whenever you can. It makes your life easier. 

Unit-testing contracts with mocks

Mocking a contract essentially means creating a second version of that contract which behaves very similar to the original one, but in a way that can be easily controlled by the developer. You often end up with complex contracts where you only want to unit-test small parts of the contract. The problem is what if testing this small part requires a very specific contract state that is difficult to end up in?

You could write complex test setup logic everytime that brings in the contract in the required state or you write a mock. Mocking a contract is easy with inheritance. Simply create a second mock contract that inherits from the original one. Now you can override functions to your mock. Let us see it with an example.

Example: Private ERC20

We use an example ERC-20 contract that has an initial private time. The owner can manage private users and only those will be allowed to receive tokens at the beginning. Once a certain time has passed, everyone will be allowed to use the tokens. If you are curious, we are using the _beforeTokenTransfer hook from the new OpenZeppelin contracts v3.

pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract PrivateERC20 is ERC20, Ownable {
    mapping (address => bool) public isPrivateUser;
    uint256 private publicAfterTime;

    constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public {
        publicAfterTime = now + privateERC20timeInSec;
    }
    
    function addUser(address user) external onlyOwner {
        isPrivateUser[user] = true;
    }

    function isPublic() public view returns (bool) {
        return now >= publicAfterTime;
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override {
        super._beforeTokenTransfer(from, to, amount);

        require(_validRecipient(to), "PrivateERC20: invalid recipient");
    }

    function _validRecipient(address to) private view returns (bool) {
        if (isPublic()) {
            return true;
        }

        return isPrivateUser[to];
    }
}

And now let's mock it.

pragma solidity ^0.6.0;
import "../PrivateERC20.sol";

contract PrivateERC20Mock is PrivateERC20 {
    bool isPublicConfig;

    constructor() public PrivateERC20(0) {}

    function setIsPublic(bool isPublic) external {
        isPublicConfig = isPublic;
    }

    function isPublic() public view returns (bool) {
        return isPublicConfig;
    }
}

You will get one of the following error messages:

  •  PrivateERC20Mock.sol: TypeError: Overriding function is missing "override" specifier.
  •  PrivateERC20.sol: TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?

Since we are using the new 0.6 Solidity version, we have to add the virtual keyword for functions that can be overriden and override for the overriding function. So let us add those to both isPublic functions.

Now in your unit tests, you can use PrivateERC20Mock instead. When you want to test the behaviour during the private usage time, use setIsPublic(false) and likewise setIsPublic(true) for testing the public usage time. Of course in our example, we could just use time helpers to change the times accordingly as well. But the idea of mocking should be clear now and you can imagine scenarios where it is not as easy as simply advancing the time.

Mocking many contracts

It can become messy if you have to create another contract for every single mock. If this bothers you, you can take a look at the MockContract library. It allows you to override and change behaviours of contracts on-the-fly. However, it works only for mocking calls to another contract, so it would not work for our example.

Mocking can be even more powerful

The powers of mocking do not end there.

  • Adding functions: Not only overriding a specific function is useful, but also just adding additional functions. A good example for tokens is just having an additional mint function to allow any user to get new tokens for free.
  • Usage in testnets: When you deploy and test your contracts on testnets together with your Dapp, consider using a mocked version. Avoid overriding functions unless you really have to. You want to test the real logic after all. But adding for example a reset function can be useful that simply resets the contract state to the beginning, no new deployment required. Obviously you would not want to have that in a mainnet contract.

Markus Waas

Solidity Developer

More great blog posts from Markus Waas

  • MultiTrade

    MultiSwap: How to arbitrage with Solidity

    Making multiple swaps across different decentralized exchanges in a single transaction

    If you want maximum arbitrage performance, you need to swap tokens between exchanges in a single transaction. Or maybe you just want to save gas on certain swaps you perform regularly. Or maybe you have your own custom use case for swapping between decentralized exchanges. And of course maybe you...

  • Optimism Ethereum

    The latest tech for scaling your contracts: Optimism

    How the blockchain on a blockchain works and how to use it

    Have you heard of Optimism? The new Optimistic VM enables Plasma but for smart contracts! What does that mean? Well read on. But what it enables is having a side chain with guarantees of the Ethereum mainnet chain. How cool is that? And you can already use it for several apps on mainnet....

  • Aurora NEAR Protocol

    Ultimate Performance: The Aurora Layer2 Network

    Deploying and onboarding users to the Aurora Network powered by NEAR Protocol

    We've covered several Layer 2 sidechains before: Polygon xDAI Binance Smart Chain But today might be the fastest of them all. On top it's tightly connected to the NEAR protocol ecosystem, a PoS chain with a scalable sharding design. And of course they have a bridge to Ethereum! What is the Aurora...