Skip to content

Migrate Mocha tests from Hardhat 2

This guide walks you through the changes needed to migrate your Mocha tests from Hardhat 2 to Hardhat 3.

Hardhat 3 is ESM-first, so JavaScript test files (.js) are treated as ES modules. See the ESM migration guide for the conversion steps, or for how to keep your tests as CommonJS with a .cjs extension.

If you keep your tests as CommonJS, you can’t require("hardhat") at the top level. Use a dynamic import from a before hook instead:

describe("suite", function () {
let hre;
before(async function () {
({ default: hre } = await import("hardhat"));
});
it("some test", async function () {
// use the hre
});
});

One of the biggest changes in Hardhat 3 is how network connections work. You don’t have a single, global network connection anymore. Instead, you create and manage network connections explicitly. For example, if you have a test that uses the JSON-RPC provider:

describe("suite", function () {
it("some test", async function () {
const blockNumber = await hre.network.provider.send("eth_blockNumber");
});
});

You now have to create a network connection first:

describe("suite", function () {
it("some test", async function () {
const { provider } = await hre.network.create();
const blockNumber = await provider.send("eth_blockNumber");
});
});

Most of the time, you’ll want to use the same provider for multiple tests. If you’re using ESM syntax, you can use top-level await and define it as a shared variable:

const { provider } = await hre.network.create();
describe("suite", function () {
it("some test", async function () {
const blockNumber = await provider.send("eth_blockNumber");
});
});

If you’re using CommonJS modules instead, you can define the provider in a before block:

let provider;
describe("suite", function () {
before(async function () {
({ provider } = await hre.network.create());
});
it("some test", async function () {
const blockNumber = await provider.send("eth_blockNumber");
});
});

In Hardhat 2, the hardhat-ethers plugin adds an ethers object to the Hardhat Runtime Environment, meaning you can do things like this in a test:

const hre = require("hardhat");
describe("suite", function () {
it("some test", async function () {
const contract = await hre.ethers.deployContract("MyContract");
});
});

In Hardhat 3, this same ethers object is included as part of each created network connection:

import hre from "hardhat";
describe("suite", function () {
let ethers;
before(async function () {
({ ethers } = await hre.network.create());
});
it("some test", async function () {
const contract = await ethers.deployContract("MyContract");
});
});

Some of the Chai matchers have changed in Hardhat 3, to make them work with multiple network connections.

These changes are:

  • Some matchers take an instance of HardhatEthers (i.e. the ethers object from the network connection), as shown in this list.
  • .reverted is now .revert(ethers)
  • .revertedWithoutReason() is now .revertedWithoutReason(ethers)
  • changeEtherBalance, changeEtherBalances, changeTokenBalance, and changeTokenBalances now take an instance of HardhatEthers as the first argument.

The @nomicfoundation/hardhat-network-helpers package is now a plugin. Instead of importing its functions directly, now you get the helpers as part of the network connection:

import { network } from "hardhat";
const { networkHelpers } = await network.create();
await networkHelpers.mine(5);