Testing a Smart Contract

Table of contents

No heading

No headings in the article.

Smart contracts are series of operation programmed to carry out a particular logic. While this is a typical feature of any software, to carry out an operation, smart contracts are different because all interaction to the logic of smart contracts cost money, excepts for a view function not called by a smart contract. And with the programmed smart contract being an account, it is susceptible to attack and funds could be stolen.

Due to the possibility of a smart contract having a bug, it is important to run a test on the logic in a contract so as to find out possible errors. In this article, you will understand what test do, understand different kind of tests, and will also walk you through some testing operation with a framework.

Why You Must Test Your Smart Contracts?

Test aids programmers in checking the logic in their codes. These tests are programmed scenarios to help ascertain the effectiveness of an algorithm. They could be programmed to check that a function that does addition when passed in two parameters, for example, sum(2,2) should equal 4.

These set of operations are important to help unravel the presence of bugs. Test being a pointer of bugs, they are crucial for your smart contracts in order to check for possible vulnerabilities. The correctness of a logic is discovered when you run multiple thorough tests of different scenarios.

Different Kinds of Tests

There are different kind of tests that exist, depending on the number of operations under observaton. While it is certain that despite carrying out tests and pushing your codebase for auditing to reduce bugs, there are possibility of discovering novel vulnerability. Possible test scenarios are as follows:

UNIT-TESTING

Unit-testing is a form of testing a fraction or small units of your application locally. This scrutinizes the correctness of an operation in a software. In a test-driven ecosystem, minute parts of a software are tested to ensure that it correlates with the coded logic.

TESTS FOR INTEGRATION

When features and libraries are integrated into smart contracts, the integration test expects to know the validity of logic. You can test that the Openzeppelin utilities, may be Reentrancy Guard or the SafeMath library, are working appropriately as expected when integrated into your project.

FUZZ TESTING

Fuzz testing are the insertion of wrong or incorrect inputs to help discover defects in a software. It is often called fuzzing because it is a process of carrying out or interracting with applications in an unexpected scenarios in order to unravel faults in the development process.

END-TO-END FUNCTIONAL TESTING

This form of testing is a complete test to check all the functionalities in the application. It tests the application as if it is running on the mainnet. This involves forking mainnet, impersonating addresses, setting balance of an address, getting storage and warping time and much more features.

Testing Smart Contract with Hardhat

Hardhat is a fast, extensible and flexible ethereum development tool for creating, testing and deploying smart contracts. This is a Javascript framework created to help in the compilation of contracts, test for possible bugs, debug errors and deploy to different networks. The Hardhat framework comes with a lot of packages that enables you write your contract and also allows for the use of ethers to test smart contract and run scripts for deployment.

We will be testing the default contract that comes with the Hardhat framework. You can check out this for the installation.

When you successfully install hardhat, you will find the contract below in contracts/Greeter.sol.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";

contract Greeter {
    string private greeting;

    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        greeting = _greeting;
    }

    function greet() public view returns (string memory) {
        return greeting;
    }

    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }
}

The Greeter contract is a simple contract that has a state private string variable, called greeting. It has a constructor that receives a string memory variable as a parameter. This parameter sets at constructor is consoled log with the imported hardhat console and the next line sets the state string variable to the input from the constructor.

The two functions, the one with the view visibility and the one without, reads from state and writes to state respectively . The view function greet reads the present greeting value. The other, setGreeting, receives a parameter to change or set the greeting.

The default test that comes with the contract above is as follows:

import { expect } from "chai";
import { ethers } from "hardhat";

describe("Greeter", function () {
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory("Greeter");
    const greeter = await Greeter.deploy("Hello, world!");
    await greeter.deployed();

    expect(await greeter.greet()).to.equal("Hello, world!");

    const setGreetingTx = await greeter.setGreeting("Hola, mundo!");

    // wait until the transaction is mined
    await setGreetingTx.wait();

    expect(await greeter.greet()).to.equal("Hola, mundo!");
  });
});

This is a chai test. Chai is a Javascript testing framework to tests for bugs. Other popular ones are Mocha and Jest. It is important to add that with the introduction of Foundry, it is possible now to run tests all with the Solidity language.

According to the code, it first imports expect from chai and imports ethers. The describe method is to identify the function being tested. The it description notes the primary purpose for the test.

In order to tests the function in the contract, it is important to first deploy the contract and then make its functions accessible for testing. This is where ethers help in deployment. Greeter is the constant that gets the contract to be deployed, the constant greeter is passing in the value to the constructor and the next line deploys the contract.

The first expect checks that on calling the greeter function which is a read function, the outputs will be equal to the passed "Hello World!. The next line is the calling of the setGreeting function and a value passed to it. The line that follows checks that transaction is successfully mined and then it expects that the new greeting value is truly the new value.

There are other possible checks that these tests framework make available. They include expectRevert, beforeEach, greaterThan, and many more.

All of these are to check the validity of the operation whether it tallies with its intended purpose. Test assures you to a level that your contract is less prone to attack.

With this introduction to testing of smart contract, I hope that you will build the habit of testing your logic so that your contract doesn't become the hacker's next target.