Easy, Instant Mocks for Solidity Contracts

In a previous post, we learned about forking mainnet as an alternative to mock contracts in Solidity. The major drawback with mock contracts is that you need to spend time to rewrite existing smart contracts as mocks. To get exhaustive mock coverage, you’ll end up rewriting whole protocols for the sake of testing.

What if there’s a magical way to mock contract functionality without writing mock contracts? Read on to learn more.

Are you new to smart contracts? Check out the open source Hardhat starter kit! It comes with several useful tools preconfigured such as Typescript, type generation, linting, formatting, and test coverage. Just hit clone and run!

Instant Mocks with Waffle / Smock

You can instantly create mocks with the Waffle (or Smock) testing libraries. Just provide the ABI of the smart contract you want to mock:

// Waffle example

import { deployMockContract } from '@ethereum-waffle/mock-contract';
import contractABI from '../artifacts/MyContract.json'

// Set up mock contract
const myContract = await deployMockContract(wallet, contractAbi);

You can set the return values for any of your mock’s functions with mock.<nameOfMethod> helpers:

// You can mock specific functions, down to specific inputs
await myContract.mock.<nameOfMethod>.returns(<value>)
await myContract.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)

You can also set up reverts using reverts() and revertsWithReason() helpers:

await myContract.mock.<nameOfMethod>.reverts()
await myContract.mock.<nameOfMethod>.revertsWithReason(<reason>)
await myContract.mock.<nameOfMethod>.withArgs(<arguments>).reverts()
await myContract.mock.<nameOfMethod>.withArgs(<arguments>).revertsWithReason(<reason>)

Behind every mock is a real smart contract (with actual Solidity code!) This means you can modify the behavior of individual functions, and leave some functions unmocked. Any values from mocked calls will pass through to be used in the actual contract code.

Full Example

Let’s try to mock the following contract:

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
}

contract MyContract {
    IERC20 private token;
    uint private constant THRESHOLD = 1000000 * 10 ** 18;

    constructor (IERC20 _token) public {
        token = _token;
    }

    function check() public view returns (bool) {
        uint balance = token.balanceOf(msg.sender);
        return balance > THRESHOLD;
    }
}

In our unit tests, we will to mock the token.balanceOf() function to return specific values. Mock contracts will be used to supply exactly the values we need to test different scenarios of the MyContract.check() method.

Here’s our unit test using mocks:

describe("Mocking example", function () {
  beforeEach(async function () {
    this.signers = {} as Signers;
    const signers: SignerWithAddress[] = await hre.ethers.getSigners();
    this.signers.owner = signers[0];
    this.signers.receiver = signers[1];

    const MyContractArtifact: Artifact = await hre.artifacts.readArtifact('MyContract');
    this.contract = <MyContract>await deployContract(this.signers.owner, MyContractArtifact, []);

    const ERC20Artifact: Artifact = await hre.artifacts.readArtifact('MyContract');
    this.token = await deployMockContract(this.signers.owner, ERC20Artifact.abi);
  });

  it('returns false if the wallet has less then 1000000 coins', async () => {
    await this.token.mock.balanceOf.returns(utils.parseEther('999999'));
    expect(await this.contract.check()).to.be.equal(false);
  });

  it('returns true if the wallet has at least 1000000 coins', async () => {
    await this.token.mock.balanceOf.returns(utils.parseEther('1000001'));
    expect(await this.contract.check()).to.equal(true);
  });

  it('reverts if the ERC20 reverts', async () => {
    await this.token.mock.balanceOf.reverts();
    await expect(this.contract.check()).to.be.revertedWith('Mock revert');
  });

  it('returns 1000001 coins for my address and 0 otherwise', async () => {
    await this.token.mock.balanceOf.returns('0');
    await this.token.mock.balanceOf.withArgs(this.signers.owner.address).returns(utils.parseEther('1000001'));

    expect(await this.contract.check()).to.equal(true);
    expect(await this.contract.connect(this.signers.receiver.address).check()).to.equal(false);
  });
});

That’s it!

Summary

Mocks are most effectively used when you need some behavior of a real smart contract but still want the ability to modify things on the fly.

Use the ethereum-waffle (or smock) testing libraries to instantly mock smart contracts, all without writing mock contracts yourself.