Skip to content
All essays
CraftMarch 27, 202411 min

Hardhat Testing Framework

Master Hardhat for smart contract testing and development

Ü
Ümit Uz
Mobile & Full Stack Developer

Hardhat is the most popular development environment for Ethereum software. It provides tools for compiling, testing, deploying, and debugging smart contracts.

Setting Up Hardhat

Installation

bash
mkdir my-project
cd my-project
npm init -y
npm install --save-dev hardhat
npx hardhat

Project Structure

my-project/
├── contracts/
│   └── MyContract.sol
├── scripts/
│   └── deploy.js
├── test/
│   └── MyContract.test.js
├── hardhat.config.js
└── .env

Configuration

javascript
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {
      chainId: 1337
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY
  }
};

Writing Tests

Basic Test

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

describe("MyContract", function () {
  let myContract;
  let owner;

  beforeEach(async function () {
    [owner] = await ethers.getSigners();
    const MyContract = await ethers.getContractFactory("MyContract");
    myContract = await MyContract.deploy();
  });

  it("Should set the right owner", async function () {
    expect(await myContract.owner()).to.equal(owner.address);
  });

  it("Should deploy with initial value", async function () {
    expect(await myContract.value()).to.equal(0);
  });
});

Testing State Changes

javascript
it("Should update value correctly", async function () {
  await myContract.setValue(42);
  expect(await myContract.value()).to.equal(42);
});

it("Should revert with invalid input", async function () {
  await expect(
    myContract.setValue(0)
  ).to.be.revertedWith("Value must be positive");
});

Testing Events

javascript
it("Should emit Transfer event", async function () {
  await expect(myContract.transfer(recipient.address, 100))
    .to.emit(myContract, "Transfer")
    .withArgs(owner.address, recipient.address, 100);
});

Advanced Testing

Testing with Multiple Accounts

javascript
describe("Access control", function () {
  it("Should only allow owner to call", async function () {
    const [owner, user1] = await ethers.getSigners();

    await myContract.connect(owner).adminFunction();
    await expect(
      myContract.connect(user1).adminFunction()
    ).to.be.revertedWith("Not owner");
  });
});

Testing Reverts

javascript
it("Should revert when insufficient balance", async function () {
  await expect(
    myContract.withdraw(1000)
  ).to.be.revertedWithCustomError(
    myContract,
    "InsufficientBalance"
  ).withArgs(1000, 100);
});

Time Manipulation

javascript
it("Should work with time", async function () {
  await myContract.setLockTime(3600);

  // Fast forward 1 hour
  await ethers.provider.send("evm_increaseTime", [3600]);
  await ethers.provider.send("evm_mine");

  await myContract.withdraw();
});

Testing Utilities

Helper Functions

javascript
const { time, loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

async function deployContractFixture() {
  const [owner, user1, user2] = await ethers.getSigners();
  const Contract = await ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy();
  return { contract, owner, user1, user2 };
}

describe("MyContract", function () {
  it("Should work", async function () {
    const { contract, owner } = await loadFixture(deployContractFixture);
    // Test
  });
});

Balance Assertions

javascript
it("Should transfer ETH correctly", async function () {
  const [owner, recipient] = await ethers.getSigners();

  await expect(() =>
    contract.transfer(recipient.address, { value: ethers.parseEther("1.0") })
  ).to.changeEtherBalances(
    [owner, recipient],
    [ethers.parseEther("-1.0"), ethers.parseEther("1.0")]
  );
});

Gas Testing

Gas Snapshots

javascript
it("Should match gas snapshot", async function () {
  const tx = await contract.function();
  expect(tx).to.have.gasCost(50000);
});

Gas Comparison

javascript
it("Should optimize gas", async function () {
  const tx1 = await contract.expensiveFunction();
  const tx2 = await contract.optimizedFunction();

  expect(tx2.gasUsed).to.be.lt(tx1.gasUsed);
});

Integration Testing

Mainnet Forking

javascript
it("Should work with mainnet fork", async function () {
  await hre.network.provider.request({
    method: "hardhat_reset",
    params: [{
      forking: {
        jsonRpcUrl: process.env.MAINNET_RPC_URL
      }
    }]
  });

  const contract = await ethers.getContractAt(
    "IERC20",
    "0x6B175474E89094C44Da98b954EedeAC495271d0F" // DAI
  );

  const balance = await contract.balanceOf("vitalik.eth");
  console.log(balance.toString());
});

Coverage

Running Coverage

bash
npx hardhat coverage

Coverage Thresholds

javascript
// hardhat.config.js
module.exports = {
  solidity: "0.8.19",
  coverage: {
    threshold: {
      lines: 80,
      statements: 80,
      branches: 80,
      functions: 80
    }
  }
};

Debugging

Console Logging

solidity
// Import hardhat console
import "hardhat/console.sol";

contract MyContract {
    function debug() public {
        console.log("Debug message");
        console.logUint(123);
    }
}

Tracing

javascript
it("Should trace transaction", async function () {
  const tx = await contract.function();
  const receipt = await tx.wait();

  console.log(receipt);
});

Deployment Scripts

Simple Deploy

javascript
async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with:", deployer.address);

  const Contract = await ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy();

  await contract.deployed();
  console.log("Contract deployed to:", contract.address);
}

main().catch(console.error);

Verified Deploy

javascript
async function main() {
  const Contract = await ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy();

  await contract.deployed();
  console.log("Contract deployed to:", contract.address);

  // Wait for confirmations
  await contract.deployTransaction.wait(5);

  // Verify on Etherscan
  await hre.run("verify:verify", {
    address: contract.address,
    constructorArguments: []
  });
}

Best Practices

  1. 1Write tests before or alongside contract code
  2. 2Test both success and failure cases
  3. 3Use descriptive test names
  4. 4Test edge cases and boundary conditions
  5. 5Keep tests independent
  6. 6Use fixtures for setup
  7. 7Test gas consumption
  8. 8Mock external contracts
  9. 9Test access control
  10. 10Verify events emission

Conclusion

Hardhat provides everything needed for professional smart contract development. Master its testing framework, and you'll build more reliable, secure contracts with confidence.

Enjoyed this essay? Share it.

Related essays

Next essay
Advanced GraphQL Federation Patterns