Smart Contract Testing with Waffle 3

What are the features of Waffle and how to use them.

Waffle has been a relatively recent new testing framework, but has gained a lot of popularity thanks to its simplicity and speed. Is it worth a try? Absolutely. I wouldn't run and immediately convert every project to it, but you might want to consider it for new ones. It's also actively being developed and continuously improving.

1. Requirements: Install the tools

First let's create a new project and initialize it with Buidler.

$ npm init
$ npm install --save-dev @nomiclabs/buidler

Now create a Buidler project by running npx buidler and choose the example project. Afterwards, you can install the required dependencies.

$ npm install --save-dev @nomiclabs/buidler-waffle ethereum-waffle chai @nomiclabs/buidler-ethers ethers

We are almost finished with the setup. Let's change the package.json scripts. Add the npx buidler test command to your package.json scripts and see if everything is working by running npm test.

2. Waffle testing

The default contract created by Buidler is a simple Greeter contract. On the right is the extended version of it:

  • You can set and read a greeting variable.
  • You see a way to utilize the Buidler console.log.
  • Added a number variable and a respective setter.


This will be our example contract for the testing. The basic example test is given below under test/sample-test.js:

const { expect } = require("chai");

describe("Greeter", () => {
  it("Should return the new greeting", async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello world!");
    expect(await greeter.greet()).to.equal("Hello world!");

    await greeter.setGreeting("Hola, mundo!");
    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.8;

import "@nomiclabs/buidler/console.sol";

contract Greeter {
    string greeting;
    uint256 public number;

    constructor(string memory _greeting) public {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }

    function setNumber(uint256 _number) public {
        number = _number;
    }
}

You can either use the Buidler functionality of getContractFactory. This is an injected function by Buidler to help you. Or you could use the native Waffle deployContract which is available automatically as waffle.deployContract in Buidler. If you wanted to go down that path, make sure to the correct path to your ABI (./artifacts in Buidler), see here for an example.

2.1 Waffle matching helpers

The test above shows you the basic setup, but doesn't really use a lot of Waffle features yet. One very useful feature present in Waffle is the automatic matching. They are similar what the @openzeppelin/test-helpers are for Truffle.

Available matchers exist for

Now let's use the BigNumbers matcher as example. You don't have to do anything, you can just the .to.equal and it will work for BigNumbers as well.

Let's also move the deployment into a before hook. This way our contract is only deployed once.

const { expect } = require("chai");

describe("Greeter", () => {
  let greeter;

  before(async () => {
    const Greeter = await ethers.getContractFactory("Greeter");
    greeter = await Greeter.deploy("Hello world!");
    await greeter.deployed();
  });

  it("Should return the new greeting", async () => {
    expect(await greeter.greet()).to.equal("Hello world!");

    await greeter.setGreeting("Hola, mundo!");
    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });

  it("Should set the number", async () => {
    expect(await greeter.number()).to.equal("0");

    await greeter.setNumber(123);
    expect(await greeter.number()).to.equal("123");
  });
});

2.2 Waffle fixtures to speed up running time

Now you may have noticed that there's one problem with using the before hook as done above. What do you think would happen if we added the following line to the second test (Should set the number)?

 expect(await greeter.greet()).to.equal("Hello world!");

That's right, it would fail. Why?

Because the before hook is run only once, so in our first test with setGreeting("Hola, mundo!") we actually change the state also for the second test. The easiest solution?

Just use beforeEach instead of before. This works as it deploys a new contract before each test. The drawback?

Performance!


fast

This is where Waffle comes in. We can define fixtures that create certain scenarios. For example our fixture:

async function fixture([wallet]) {
  const greeter = await deployContract(
    wallet, Greeter, ["Hello world!"]
  );
  return { greeter, wallet };
}

This fixture just deploys our Greeter contract, but with a catch. A snapshot of our blockchain is taken after running this fixture. So afterwards with loadFixture we don't actually run time-intensive redeploments.

Rather we simply load the previously stored snapshot of the blockchain. This makes our runtimes significantly faster than using beforeEach.

Please note: Unfortuntately currently the buidler-integrated loadFixture is broken as discovered by PaulRBerg. The solution is to use createFixtureLoader as shown on the right.

const { expect } = require("chai");
const { deployContract, MockProvider } = require("ethereum-waffle");
const { waffle } = require("@nomiclabs/buidler");
const Greeter = require("../artifacts/Greeter.json");

const myProvider = new MockProvider();
const loadFixture = waffle.createFixtureLoader(
  myProvider.getWallets(),
  myProvider
);

describe("Greeter", () => {
  async function fixture([wallet]) {
    const greeter = await deployContract(wallet, Greeter, ["Hello world!"]);
    return { greeter, wallet };
  }

  it("Should return the new greeting once it's changed", async () => {
    const { greeter } = await loadFixture(fixture);

    expect(await greeter.greet()).to.equal("Hello world!");

    await greeter.setGreeting("Hola, mundo!");
    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});

2.3 Waffle mocking

On top of mocking as with Truffle contracts, in Waffle you can mock contract calls automatically. It's like an integrated gnosis/mock-contract. Instead of deployContract you just use deployMockContract when deploying in your tests. Then you have the ability to change the return values of contract calls.

One drawback is that this works only when calling a contract within a contract. So given our Greeter example, to use the feature we need to add a GreetManager contract. Let's also add a string parameter to the greet function. As you can see, we can

  • mock every call using mockGreeter.mock.greet.returns("mockedReturn 1")
  • or mock depending on the passed arguments using mockGreeter.mock.greet .withArgs("argument") .returns("mockedReturn 2")
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.8;

import "./Greeter.sol";

contract GreetManager {
    Greeter greeter;

    constructor(Greeter greeter_) public {
        greeter = greeter_;
    }

    function useGreeter(string memory custom)
        public
        view
        returns (string memory)
    {
        return greeter.greet(custom);
    }
}
const { expect } = require("chai");
const {
  deployContract,
  deployMockContract,
  MockProvider,
} = require("ethereum-waffle");
const Greeter = require("../artifacts/Greeter.json");
const GreetManager = require("../artifacts/GreetManager.json");

const wallet = new MockProvider().getWallets()[0];

describe("Greeter", () => {
  it("Should return what is set by mock", async () => {
    const mockGreeter = await deployMockContract(wallet, Greeter.abi, [
      "Hello world!",
    ]);
    const greetManager = await deployContract(wallet, GreetManager, [
      mockGreeter.address,
    ]);

    await mockGreeter.mock.greet.returns("Hello new world!");

    await mockGreeter.mock.greet
      .withArgs("Hello old world!")
      .returns("Hello new (not old) world!");

    expect(await greetManager.useGreeter("custom")).to.equal(
      "Hello new world!"
    );
    expect(await greetManager.useGreeter("Hello old world!")).to.equal(
      "Hello new (not old) world!"
    );
  });
});

2.3 ENS (Ethereum Name Service) 

ENS is nowadays quite often supported. You can register domains and subdomains in it. Many projects like MetaMask support this, so you can use simple names instead of addresses. If you are using it in your contracts, you can easily deploy the complete ENS smart contracts system with Waffle. 

const provider = new MockProvider();
await provider.setupENS();

This is all you need for the setup. Now you can create domains and subdomains:

await ens.createTopLevelDomain('com');
await ens.createSubDomain('soliditydeveloper.com');
await ens.createSubDomain('vitalik.buterin.eth', {recursive: true}); // all in one

And you can set addresses via:

// name to address
await ens.setAddress('soliditydeveloper.com', '0x001...03');

// address to name
await ens.setAddressWithReverse('soliditydeveloper.com', '0x001...03');

Waffle or Truffle?

I think both are viable options these days. Both have unique features. In particular I like the Waffle fixtures as they can speed up test runs significantly. If you feel like that's something you don't really need, Truffle might be the better option just due to more active developments, more tooling and support.

You should also consider the size of the project and how long it will be maintained in the future. It's certainly an option to use only Waffle. For example Uniswap v2 core being a smaller project exists purely with Waffle: https://github.com/Uniswap/uniswap-v2-core.

Or once again, why choose? Just use Waffle and Buidler for testing and Truffle for deployments / tool support.

Truffle Waffles: Yum!

Truffle-Waffle

Markus Waas

Solidity Developer

More great blog posts from Markus Waas

© 2024 Solidity Dev Studio. All rights reserved.

This website is powered by Scrivito, the next generation React CMS.