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