Testing

Writing Unit Tests

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:

npm install --save-dev mocha chai

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 instance
    beforeEach(async function () {
        [owner, addr1, addr2] = await ethers.getSigners(); // Retrieve test accounts
        Token = await ethers.getContractFactory("MyToken");
        token = await Token.deploy(); // Deploy the contract
    });

    describe("Deployment", function () {
        it("Should set the correct owner", async function () {
            expect(await token.owner()).to.equal(owner.address);
        });

        it("Should assign the total supply of tokens to the owner", async function () {
            const ownerBalance = await token.balanceOf(owner.address);
            expect(await token.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("Transactions", function () {
        it("Should transfer tokens between accounts", async function () {
            // Transfer 50 tokens from owner to addr1
            await token.transfer(addr1.address, 50);
            const addr1Balance = await token.balanceOf(addr1.address);
            expect(addr1Balance).to.equal(50);
        });

        it("Should fail if sender doesn’t have enough tokens", async function () {
            const initialOwnerBalance = await token.balanceOf(owner.address);

            // Attempt to transfer 1 token from addr1 (has 0 tokens) to addr2
            await expect(
                token.connect(addr1).transfer(addr2.address, 1)
            ).to.be.revertedWith("Insufficient balance");

            // Ensure owner's balance remains unchanged
            expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
        });

        it("Should update balances after transfers", async function () {
            const initialOwnerBalance = await token.balanceOf(owner.address);

            // Transfer 100 tokens from owner to addr1
            await token.transfer(addr1.address, 100);

            // Transfer 50 tokens from addr1 to addr2
            await token.connect(addr1).transfer(addr2.address, 50);

            const finalOwnerBalance = await token.balanceOf(owner.address);
            expect(finalOwnerBalance).to.equal(initialOwnerBalance - 100);

            const addr1Balance = await token.balanceOf(addr1.address);
            expect(addr1Balance).to.equal(50);

            const addr2Balance = await token.balanceOf(addr2.address);
            expect(addr2Balance).to.equal(50);
        });
    });
});

Breakdown of the Test Cases:

  1. 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.

  2. 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:

npx hardhat test

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.