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.

Last updated