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
- Project Setup: Install Hardhat and initialize a new project using
npx hardhat init
. - Write your Smart Contract: Develop your smart contract using Solidity, the primary language for writing smart contracts on Ethereum.
- Create the Test File: Within your project, create a directory named
test
and a file namedYourContract.test.js
(replace “YourContract” with your contract’s name). - Import Dependencies: Import necessary modules like
ethers
,chai
, andhardhat/network
. - Deploy Contract: Use
ethers
to deploy your contract to the test network. - 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 namedbalances
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 aspublic
andpayable
, 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 6:
- 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 thetransfer
function.
- Line 10:
- 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
).
- Line 16:
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.
- Gets the contract factory for
- 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 15-18: Test 1: Checks if depositing funds increases the balance.
- 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”.
- Lines 26-29: Test 1: Checks if withdrawing a valid amount decreases the balance.
Resources and Next Steps
Here are some resources to help you get started:
- Hardhat Testing Guide: https://hardhat.org/tutorial/testing-contracts
- Hardhat Network Helpers: https://hardhat.org/hardhat-network-helpers
- Ethers.js Documentation: https://docs.ethers.org/v5/
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.