Testing of Smart Contract

Important terminologies to understand:

  • describe: A describe block is used for organizing test cases in logical groups of tests. For example, we want to group all the tests for a specific class.
  • it: Defining tests using its function.
  • beforeEach: Used for code reusability and reducing redundancy of code. Create a token.js file in the test directory( folder ).

Paste the below code into the token.js file:

Solidity




const { expect } = require("chai");
describe("Token contract", function () {
   it("Deployment should assign the total supply of tokens to the owner", async function () {
     const [owner] = await ethers.getSigners();
     const Token = await ethers.getContractFactory("Token");
     const hardhatToken = await Token.deploy();
     const ownerBalance = await hardhatToken.balanceOf(owner.address);
     expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
   });
 });


In your command prompt or terminal run npx hardhat test, and the following output is expected:

 Token contract   
✓ Deployment should assign the total supply of tokens to the owner (654ms)
1 passing (663ms)

Which means the test has been passed.

 const [owner] = await ethers.getSigners();

In ethers.js, a Signer is an object that represents an Ethereum account. Transactions are sent to contracts and other accounts using this method. We’re obtaining a list of the accounts in the node we’re connected to, which is Hardhat Network in this case, and we’re just saving the first one. The global scope includes the ethers variable. If you prefer that your code be always explicit, you may add the following line at the top:

const { ethers } = require("hardhat");
const Token = await ethers.getContractFactory("Token");

A ContractFactory is used to create an instance of a token contract. Token here is just the instance.

const hardhatToken = await Token.deploy();

Calling the deploy() function on a ContractFactory instance will start the deployment process and returns a Promise. This object that gets created has a method for each of your smart contract functions.

const ownerBalance = await hardhatToken.balanceOf(owner.address);

After deploying the contract, contract methods on hardhatToken can be easily called. By calling the balanceOf() method we can extract the owner’s account balance.

It is important to understand that the account that deploys the token gets its entire supply. And by default instances are connected to the first signer.

expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);

Here it is IMPORTANT to note that the total supply and owner’s balance should be equal. Therefore to check this, we wrote the above code statement, which will tell you whether the contract is correctly deployed or not.
To do this we’re using Chai which is a popular JavaScript assertion library. These asserting functions are called “matchers“, and the ones we’re using here come from the @nomicfoundation/hardhat-chai-matchers plugin, which extends Chai with many matchers useful to test smart contracts.

Using a Different Account

To test your code by sending a transaction from an account (or Signer in ethers.js terminology) other than the default one, you can use the connect() method on your ethers.js Contract object to connect it to a different account, like this:

Javascript




const { expect } = require("chai");
 describe("Token contract", function () {
   // ...previous test...
    
   it("Should transfer tokens between accounts",async () => {
    
     // getSigners is used to get the account addresses and there balances.
     [owner,addr1,addr2] = await ethers.getSigners();
      
     // Creating instance of the contract.
     // getContractFactory is used to create instance of the contract.
     Token = await ethers.getContractFactory("Token");
      
     // Deploying the above instance over hardhat platform provided test local blockchain.
     hardhatToken = await Token.deploy();
      
     // Transfer 10 tokens from owner to addr1
     await hardhatToken.transfer(addr1.address,10);
      
     // extracting balance assigned to addr1 after deployment.
     const addr1Balance = await hardhatToken.balanceOf(addr1.address);
     expect(addr1Balance).to.equal(10);
      
     // Transfer 5 tokens from addr2 to addr1
     await hardhatToken.connect(addr1).transfer(addr2.address,5);
      
     // extracting balance assigned to addr2 after deployment.
     const addr2Balance = await hardhatToken.balanceOf(addr2.address);
     expect(addr2Balance).to.equal(5);
      
   }).timeout(50000);
 });


Full Test Suite

The below Code is the full test suite for Token.sol with other added information and the tests to be performed are structured in comprehended format.

Javascript




const {expect} = require("chai"); // Mocha-framework, chai-library
const { ethers } = require("hardhat");
 
 // basic syntax just to describe the contract name you can write any name over here.
describe("Token contract", function () {
   
    let Token;
    let hardhatToken;
    let owner;
    let addr1;
    let addr2;
    let addrs;
 
    /* beforeEach is a hook provided by mocha blockchain to attach common
     part before every describe function.*/
    /* common block to define, declare, intialize every common requirement such as to deploy,
    create an instance of blockchain.*/
    beforeEach(async () => {
        // getSigners is used to get the account addresses and there balances.
        [owner,addr1,addr2,...addrs] = await ethers.getSigners();
        
        // Creating instance of the contract.
        // getContractFactory is used to create instance of the contract.
        Token = await ethers.getContractFactory("Token");
        
        // Deploying the above instance over hardhat platform provided test local blockchain.
        hardhatToken = await Token.deploy();
    });
 
 
    describe('Deployment', () => {
        // it - is used to perform test over every function.
        // For testing every function we define 'it'.
        // Below 'it' checks if the deployment is done perfectly over a constructor call.
        it("Should set the right owner", async () => {
            expect(await hardhatToken.owner()).to.equal(owner.address);
        }).timeout(50000);
 
        it("Deployment should assign the total supply of tokens to the owner", async () => {
   
            /* Checking if the totalSupply is assigned to the owner and
            owner's balance has been credited.*/
            // extracting balance assigned to owner after deployment.
            const ownerBalance = await hardhatToken.balanceOf(owner.address);
         
            // Testing if( ownerBalance == totalSupply())
            /* if this doesn't happens to be true it will show 1 failing with
            AssertionError: Expected "10000" to be equal 10 */
            expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
            
        }).timeout(50000);
    });
 
 
    describe('Transaction', () => {
        it("Should transfer tokens between accounts",async () => {
   
            // Transfer 10 tokens from owner to addr1
           await hardhatToken.transfer(addr1.address,10);
           // extracting balance assigned to addr1 after deployment.
           const addr1Balance = await hardhatToken.balanceOf(addr1.address);
           expect(addr1Balance).to.equal(10);
         
            // Transfer 5 tokens from addr2 to addr1
            await hardhatToken.connect(addr1).transfer(addr2.address,5);
            // extracting balance assigned to addr2 after deployment.
            const addr2Balance = await hardhatToken.balanceOf(addr2.address);
            expect(addr2Balance).to.equal(5);
 
        }).timeout(50000);
 
        it("Should fail if sender doesnot have enough tokens",async () => {
            
           const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
           await expect(hardhatToken.connect(addr1).transfer(owner.address,1)).to.be.revertedWith("Not enough tokens");
         
           expect(await hardhatToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
 
        }).timeout(50000);
 
 
        it("Should update balances after transfers",async () => {
   
           const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
           await hardhatToken.transfer(addr1.address,5);
           await hardhatToken.transfer(addr2.address,10);
           const finalOwnerBalance = await hardhatToken.balanceOf(owner.address);
           expect(finalOwnerBalance).to.equal(initialOwnerBalance-15);
           const addr1Balance = await hardhatToken.balanceOf(addr1.address);
           expect(addr1Balance).to.equal(5);
 
           const addr2Balance = await hardhatToken.balanceOf(addr2.address);
        
           expect(addr2Balance).to.equal(10);
 
        }).timeout(50000);
    });
 
   
});


Run npx hardhat test in your command prompt/ terminal. The output of npx hardhat test should look like this:

Fig 1.3. Results of the Tests Performed

Note : npx hardhat test would automatically compile the last updated code.

Debugging

Debugging of solidity smart contracts is possible by logging messages by calling console.log() from solidity code while running smart contracts and tests on Hardhat Network. Hardhat provides a console.sol module which makes it possible to log messages from solidity code. To do so, you just need to import hardhat/console.sol. This is how it should look like:

Solidity




pragma solidity >=0.5.0 <0.9.0;
import "hardhat/console.sol";
contract Token { //...
 
 }


Then you can simply add console.log to solidity functions similar to using it in Javascript. Here we are using it in the transfer() function.

Solidity




function transfer(address to, uint256 amount) external {
       require(balances[msg.sender]>=amount, "Not enough tokens");
 
       console.log( "Transferring from %s to %s %s tokens",msg.sender,to,amount);
 
       balances[msg.sender] -= amount;
       balances[to] += amount;
 
   }


Logging outputs will be reflected when you run your tests:

Fig 1.4. Debugging the solidity code

How to Test a Smart Contract for Ethereum?

Public Blockchains like Ethereum are immutable, it is difficult to update the code of a smart contract once it has been deployed. To guarantee security, smart contracts must be tested before they are deployed to Mainnet. There are several strategies for testing contracts, but a test suite comprised of various tools and approaches is suitable for detecting both minor and significant security issues in contract code.

Similar Reads

Smart Contract Testing

Smart contract testing is the process of ensuring that a smart contract’s code operates as intended. Testing is important for determining whether a certain smart contract meets standards for dependability, usability, and security. Approaches may differ, but most testing methods need to run a smart contract with a subset of the data it promises to handle. It is presumed that the contract is working properly if it gives correct outcomes for sample data. Most testing tools include resources for creating and executing test cases to determine if a contract’s execution matches the intended outcomes....

Importance of Testing a Smart Contract

Testing Smart Contracts is a critical and significant process in the development phase since it involves deploying it on the network every time and determining whether it works as expected or whether it needs some fine-tuning to enhance and satisfy its requirements....

Problems Due to Insufficient Smart Contracts Testing

Not testing smart contracts thoroughly can lead to various problems and vulnerabilities, including:...

Methods for Smart Contract Testing

Automated and manual testing approaches for Ethereum smart contracts can be coupled to develop a strong framework for contract analysis....

Formal Verification and Smart Contract Testing

Testing can help validate a smart contract’s expected behaviour for certain data inputs, but it cannot provide definitive proof for inputs that were not included in the tests. Therefore, testing alone cannot ensure the complete “functional correctness” of a program, meaning it cannot guarantee that the program will behave as intended for all possible input values....

Testing Tools and Libraries

Here are some tools and libraries for Unit Testing smart contracts:...

How to Perform Unit Tests on Smart Contracts?

Unit Testing is the most crucial testing method to be carried out of every other testing method; There are many frameworks, tools, and libraries that help you in testing your application one of which is Hardhat. Hardhat helps in compiling and testing smart contracts at the very core. And also has its own built-in Hardhat Network (Local Ethereum Network design for development purposes). It also allows you to deploy contracts, run tests, and debugging of code....

Testing of Smart Contract

...

Manual Testing: Deploying on Live Test Network & Local Hardhat Blockchain Environment

...

Contact Us