Unit tests are essential to ensure your smart contract behaves correctly. Using Mocha and Chai in conjunction with Hardhat provides a robust framework for testing your Solidity contracts.
Install Mocha/Chai
To set up testing, install the required libraries:
npminstall--save-devmochachai
Example Test Suite: Token Contract
Here's an improved example of a test suite for a token contract:
const { expect } =require("chai");describe("Token Contract",function () {let Token, token, owner, addr1, addr2;// Before each test, deploy a new token contract instancebeforeEach(asyncfunction () { [owner, addr1, addr2] =awaitethers.getSigners(); // Retrieve test accounts Token =awaitethers.getContractFactory("MyToken"); token =awaitToken.deploy(); // Deploy the contract });describe("Deployment",function () {it("Should set the correct owner",asyncfunction () {expect(awaittoken.owner()).to.equal(owner.address); });it("Should assign the total supply of tokens to the owner",asyncfunction () {constownerBalance=awaittoken.balanceOf(owner.address);expect(awaittoken.totalSupply()).to.equal(ownerBalance); }); });describe("Transactions",function () {it("Should transfer tokens between accounts",asyncfunction () {// Transfer 50 tokens from owner to addr1awaittoken.transfer(addr1.address,50);constaddr1Balance=awaittoken.balanceOf(addr1.address);expect(addr1Balance).to.equal(50); });it("Should fail if sender doesn’t have enough tokens",asyncfunction () {constinitialOwnerBalance=awaittoken.balanceOf(owner.address);// Attempt to transfer 1 token from addr1 (has 0 tokens) to addr2awaitexpect(token.connect(addr1).transfer(addr2.address,1) ).to.be.revertedWith("Insufficient balance");// Ensure owner's balance remains unchangedexpect(awaittoken.balanceOf(owner.address)).to.equal(initialOwnerBalance); });it("Should update balances after transfers",asyncfunction () {constinitialOwnerBalance=awaittoken.balanceOf(owner.address);// Transfer 100 tokens from owner to addr1awaittoken.transfer(addr1.address,100);// Transfer 50 tokens from addr1 to addr2awaittoken.connect(addr1).transfer(addr2.address,50);constfinalOwnerBalance=awaittoken.balanceOf(owner.address);expect(finalOwnerBalance).to.equal(initialOwnerBalance -100);constaddr1Balance=awaittoken.balanceOf(addr1.address);expect(addr1Balance).to.equal(50);constaddr2Balance=awaittoken.balanceOf(addr2.address);expect(addr2Balance).to.equal(50); }); });});
Breakdown of the Test Cases:
Deployment Tests:
Ownership: Verifies that the contract's owner is correctly assigned upon deployment.
Initial Token Distribution: Confirms that the contract assigns the total token supply to the owner's balance.
Transaction Tests:
Successful Transfers: Verifies that tokens can be transferred from one account to another and the balances are updated correctly.
Failed Transfers: Ensures that a transfer will fail if the sender doesn't have enough tokens, reverting the transaction.
Balance Updates: Checks that the balances are correctly updated after multiple transactions.
Running the Tests
To run the test suite, use the following command:
This will execute all test cases and display the results, ensuring that your contract behaves as expected under various conditions.
Improvements in this Version:
Modular Tests: Divided tests into logical groups (e.g., Deployment and Transactions) for better organization and readability.
Edge Case Testing: Added test cases for failure conditions (like insufficient balances) to ensure that your contract handles errors correctly.
Reusability: Used beforeEach to deploy a fresh contract instance for each test, ensuring isolation between test cases and preventing state leakage.
This structure makes the tests more maintainable and easier to extend as your contract grows.