Blockchain Cryptocurrency Dev Tools Smart Contracts Web3
Ranjithkumar  

Testing smart contracts with hardhat

Smart contracts, the self-executing programs on blockchains, play a crucial role in decentralized applications (dApps). However, their immutability amplifies the importance of thorough testing. Bugs in a deployed contract can be costly, leading to unexpected behavior and even potential loss of funds. This is where Hardhat comes into play, offering a robust and developer-friendly environment for testing your smart contracts.

Why Test Smart Contracts?

Here’s why testing your smart contracts is absolutely essential:

  • Security: Defects in smart contracts can be exploited by malicious actors, leading to theft and financial losses. Testing helps catch these vulnerabilities early in the development cycle.
  • Confidence: Robust testing ensures your contract behaves as intended, boosting your confidence in its functionality and reliability.
  • Maintainability: Well-tested code is easier to maintain and update in the future, saving you time and resources.

Hardhat: Your Testing Partner

Hardhat provides a comprehensive suite of tools and features specifically tailored for smart contract development. Here’s how it simplifies the testing process:

  • Local Ethereum Network: Hardhat comes with a built-in local Ethereum network, eliminating the need to interact with a public blockchain for testing. This allows faster and more cost-effective development.
  • Ethers.js Integration: Hardhat integrates seamlessly with ethers.js, a popular library for interacting with Ethereum in JavaScript. This makes interacting with your smart contract during tests feel intuitive and familiar.
  • Mocha and Chai: Popular testing frameworks like Mocha and Chai are readily available within Hardhat. These tools help you structure your tests logically and assert expected behaviors effectively.
  • Custom Matchers: Hardhat provides custom Chai matchers that streamline your testing experience. These matchers simplify tasks like checking balances, verifying transactions, and asserting state changes.

Getting Started with Testing

  1. Project Setup: Install Hardhat and initialize a new project using npx hardhat init.
  2. Write your Smart Contract: Develop your smart contract using Solidity, the primary language for writing smart contracts on Ethereum.
  3. Create the Test File: Within your project, create a directory named test and a file named YourContract.test.js (replace “YourContract” with your contract’s name).
  4. Import Dependencies: Import necessary modules like etherschai, and hardhat/network.
  5. Deploy Contract: Use ethers to deploy your contract to the test network.
  6. Write Tests: Use Mocha to structure your tests and Chai assertions to verify expected behavior after calling various contract functions.

Simple Wallet

This sample contract implements a basic wallet functionality where users can deposit and withdraw funds.

pragma solidity ^0.8.0;

contract SimpleWallet {
  mapping(address => uint) public balances;

  function deposit() public payable {
    balances[msg.sender] += msg.value;
  }

  function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount, "Insufficient funds");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
  }

  function getBalance() public view returns (uint) {
    return balances[msg.sender];
  }
}
  • Line 1: pragma solidity ^0.8.0;: This line specifies the version of Solidity compiler compatible with this code.
  • Line 3: mapping(address => uint) public balances;: This line declares a public mapping named balances that stores the balance of each user (address) in Ether (uint).
  • Line 5: function deposit() public payable { ... }: This function allows users to deposit funds into their wallet. It is marked as public and payable, meaning it can be called by anyone and accepts Ether payments.
    • Line 6: balances[msg.sender] += msg.value;: This line adds the deposited amount (msg.value) to the user’s balance (balances[msg.sender]). msg.sender refers to the address of the caller.
  • Line 9: function withdraw(uint amount) public { ... }: This function allows users to withdraw funds from their wallet.
    • Line 10: require(balances[msg.sender] >= amount, "Insufficient funds");: This line checks if the user has enough funds before withdrawing. It throws an error (revert) with the message “Insufficient funds” if the balance is less than the withdrawal amount.
    • Line 11: balances[msg.sender] -= amount;: This line subtracts the withdrawn amount from the user’s balance.
    • Line 12: payable(msg.sender).transfer(amount);: This line transfers the withdrawn amount back to the user’s address using the transfer function.
  • Line 15: function getBalance() public view returns (uint) { ... }: This function allows users to view their current balance.
    • Line 16: return balances[msg.sender];: This line returns the balance of the caller (msg.sender).

Tests for SimpleWallet

This test file utilizes Mocha and Chai to test the functionality of the SimpleWallet contract.

const { expect } = require("chai");
const { ethers } = require("ethers");
const { getContractFactory } = require("hardhat");

describe("SimpleWallet", function () {
  let wallet;
  let deployer;
  let otherUser;

  beforeEach(async function () {
    const Wallet = await getContractFactory("SimpleWallet");
    wallet = await Wallet.deploy();
    await wallet.deployed();

    [deployer, otherUser] = await ethers.getSigners();
  });

  describe("deposit", function () {
    it("should deposit funds", async function () {
      const depositAmount = 100;
      await wallet.connect(deployer).deposit({ value: depositAmount });

      const balance = await wallet.getBalance();
      expect(balance).to.equal(depositAmount);
    });

    it("should revert if sent 0 ETH", async function () {
      await expect(wallet.connect(deployer).deposit()).to.be.revertedWith(
        "Ether value not specified"
      );
    });
  });

  describe("withdraw", function () {
    it("should withdraw funds", async function () {
      const depositAmount = 100;
      await wallet.connect(deployer).deposit({ value: depositAmount });

      const withdrawAmount = 50;
      await wallet.connect(deployer).withdraw(withdrawAmount);

      const balance = await wallet.getBalance();
      expect(balance).to.equal(depositAmount - withdrawAmount);
    });

    it("should revert if insufficient funds", async function () {
      const withdrawAmount = 100;
      await expect(wallet.connect(deployer).withdraw(withdrawAmount)).to.be.revertedWith(
        "Insufficient funds"
      );
    });
  });
});
  • Lines 1-3: Import necessary libraries for testing: Chai for assertions, Ethers.js for interacting with the blockchain, and Hardhat utilities for deploying contracts.
  • Line 5: describe("SimpleWallet", function () { ... }): This line defines a test suite named “SimpleWallet” that groups related tests.
  • Lines 7-11: Before each test (beforeEach), the code:
    • Gets the contract factory for SimpleWallet.
    • Deploys the contract and waits for it to be deployed.
    • Fetches the addresses of the deployer and another user for testing purposes.
  • Lines 13-22: This section tests the deposit function:
    • Lines 15-18: Test 1: Checks if depositing funds increases the balance.
      • It defines a deposit amount (100 Ether).
      • Deposits the amount using the deployer account.
      • Retrieves the balance using getBalance and asserts that it equals the deposit amount.
    • Lines 20-22: Test 2: Checks if depositing 0 Ether reverts.
      • Attempts to deposit 0 Ether and expects a revert with the message “Ether value not specified”.
  • Lines 24-34: This section tests the withdraw function:
    • Lines 26-29: Test 1: Checks if withdrawing a valid amount decreases the balance.
      • Deposits 100 Ether.
      • Withdraws 50 Ether.
      • Retrieves the balance and asserts that it reflects the remaining 50 Ether.
    • Lines 31-34: Test 2: Checks if withdrawing more than available balance reverts.
      • Attempts to withdraw 100 Ether (more than the existing balance).
      • Expects a revert with the message “Insufficient funds”.

Resources and Next Steps

Here are some resources to help you get started:

Remember, testing is an ongoing process. As you update and modify your smart contract, continually test it to ensure its continued functionality and security. By embracing a test-driven development approach with Hardhat, you can build robust and reliable smart contracts that contribute to the secure and vibrant world of blockchain technology.

Leave A Comment