>
>
Join the Hardhat team! We are hiring
>
>

#Testing with ethers.js & Waffle

TIP

Read this guide to learn about our recommended approach to testing contracts.

Writing smart contract tests in Hardhat is done using JavaScript or TypeScript.

In this guide, we'll show you how to use Ethers.js, a JavaScript library to interact with Ethereum, and Waffle a simple smart contract testing library built on top of it.

Let's see how to use it starting from an empty Hardhat project.

TIP

Ethers and Waffle support TypeScript. Learn how to set up Hardhat with TypeScript here.

# Setting up

Install Hardhat on an empty directory. When done, run npx hardhat init:

$ npx hardhat init
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.22.3

? What do you want to do? …
▸ Create a JavaScript project
  Create a TypeScript project
  Create a TypeScript project (with Viem)
  Create an empty hardhat.config.js
  Quit

Select Create an empty hardhat.config.js. This will create an empty Hardhat configuration file.

Then install chai, the @nomiclabs/hardhat-waffle plugin, and the peer dependencies of this plugin:

npm 7+
npm 6
yarn
npm install --save-dev chai@4 @nomiclabs/hardhat-waffle
npm install --save-dev chai@4 @nomiclabs/hardhat-waffle ethereum-waffle @nomiclabs/hardhat-ethers ethers@5
yarn add --dev chai@4 @nomiclabs/hardhat-waffle ethereum-waffle @nomiclabs/hardhat-ethers ethers@5

TIP

If you are using npm 7 or later, you only need to install chai and the plugin. npm will automatically install all the necessary peer dependencies.

Then open the hardhat.config.js file and require the plugin:

require("@nomiclabs/hardhat-waffle");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
};

TIP

There's no need for require("@nomiclabs/hardhat-ethers"), as @nomiclabs/hardhat-waffle already does it.

# Testing

Tests using Waffle are written with Mocha alongside Chai, two popular JavaScript testing utilities.

Before writing our test, let's add a simple contract. Create a contracts directory and then add a contracts/Greeter.sol file with this code:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Greeter {
    string private greeting;

    constructor(string memory _greeting) {
        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;
    }
}

Then create a test folder and add a test/test.js file:

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

describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");

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

    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

    // wait until the transaction is mined
    await setGreetingTx.wait();

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

In your terminal, run npx hardhat test. You should see the following output:

Greeter
Deploying a Greeter with greeting: Hello, world!
Changing greeting from 'Hello, world!' to 'Hola, mundo!'
    ✔ Should return the new greeting once it's changed (847ms)


  1 passing (851ms)

This means the test passed. Let's now explain each line:

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

We are requiring Chai, which is an assertions library. These asserting functions are called "matchers", and the ones we're using here actually come from Waffle.

This is why we're using the @nomiclabs/hardhat-waffle plugin, which makes it easier to assert values from Ethereum. Check out this section in Waffle's documentation for the entire list of Ethereum-specific matchers.

WARNING

Some Waffle matchers return a Promise rather than executing immediately. If you're making a call or sending a transaction, make sure to check Waffle's documentation, and await these Promises. Otherwise your tests may pass without waiting for all checks to complete.

describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    // ...
  });
});

This wrapper just follows Mocha's proposed structure for tests, but you might have noticed the use of async in it's callback function. Interacting with the Ethereum network and smart contracts are asynchronous operations, hence most APIs and libraries use JavaScript's Promise for returning values. This use of async will allow us to await the calls to our contract and the Hardhat Network node.

const Greeter = await ethers.getContractFactory("Greeter");

A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, so Greeter here is a factory for instances of our greeter contract.

const greeter = await Greeter.deploy("Hello, world!");

Calling deploy() on a ContractFactory will start the deployment, and return a Promise that resolves to a Contract. This is the object that has a method for each of your smart contract functions. Here we're passing the string Hello, world! to the contract's constructor.

Once the contract is deployed, we can call our contract methods on greeter and use them to get the state of the contract.

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

Here we're using our Contract instance to call a smart contract function in our Solidity code. greet() returns the greeter's greeting, and we're checking that it's equal to Hello, world!, as it should be. To do this we're using the Chai matchers expect, to and equal.

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

We can modify the state of a contract in the same way we read from it. Calling setGreeting will set a new greeting message. After the Promise is resolved, we perform another assertion to verify that the greeting change took effect.

#Testing from a different account

If you need to send a transaction from an account other than the default one, you can use the connect() method provided by Ethers.js.

The first step to do so is to get the Signers object from ethers:

const [owner, addr1] = await ethers.getSigners();

A Signer in Ethers.js is an object that represents an Ethereum account. It's used to send transactions to contracts and other accounts. Here we're getting a list of the accounts in the node we're connected to, which in this case is Hardhat Network, and only keeping the first and second ones.

TIP

To learn more about Signer, you can look at the Signers documentation.

The ethers variable is available in the global scope. If you like your code always being explicit, you can add this line at the top:

const { ethers } = require("hardhat");

Finally, to execute a contract's method from another account, all you need to do is connect the Contract with the method being executed:

await greeter.connect(addr1).setGreeting("Hallo, Erde!");

# Migrating an existing Waffle project

If you're starting a project from scratch and looking to use Waffle, you can skip this section. If you're setting up an existing Waffle project to use Hardhat you'll need to migrate the configuration options Waffle offers. The following table maps Waffle configurations to their Hardhat equivalents:

WaffleHardhat
sourcesPathpaths.sources
targetPathpaths.artifacts
solcVersionsolc.version (version number only)
compilerOptions.evmVersionsolc.evmVersion
compilerOptions.optimizersolc.optimizer

As an example, this Waffle configuration file:

{
  "sourcesPath": "./some_custom/contracts_path",
  "targetPath": "../some_custom/build",
  "solcVersion": "v0.4.24+commit.e67f0147",
  "compilerOptions": {
    "evmVersion": "constantinople",
    "optimizer": {
      "enabled": true,
      "runs": 200
    }
  }
}

Would translate into this Hardhat config:

module.exports = {
  paths: {
    sources: "./some_custom/contracts_path",
    artifacts: "../some_custom/build",
  },
  solidity: {
    version: "0.4.24", // Note that this only has the version number
    settings: {
      evmVersion: "constantinople",
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
};

If you're migrating an existing Waffle project to Hardhat, then the minimum configuration you'll need is changing Hardhat's compilation output path, since Waffle uses a different one by default:

require("@nomiclabs/hardhat-waffle");

module.exports = {
  paths: {
    artifacts: "./build",
  },
};

#Adapting the tests

Now, when testing using a standalone Waffle setup, you should use the different parts of Waffle from Hardhat.

For example, instead of doing:

const { deployContract } = require("ethereum-waffle");

You should do:

const { waffle } = require("hardhat");
const { deployContract } = waffle;

WARNING

Importing Waffle's functions from ethereum-waffle, can lead to multiple problems.

For example, Waffle has a default gas limit of 4 million gas for contract deployment transactions, which is normally too low.

Please, make sure you import them from the waffle field of the Hardhat Runtime Environment. It is a version of Waffle adapted to work well with Hardhat.

Also, you don't need to call chai.use. This initialization is already handled by @nomiclabs/hardhat-waffle. Just be sure to include require("@nomiclabs/hardhat-waffle"); in your Hardhat config.

Finally, instead of initializing a MockProvider, just use the plugin's provider like this:

const { waffle } = require("hardhat");
const provider = waffle.provider;

Run your tests with npx hardhat test and you should get stack traces when a transaction fails.