Exploring Smart Contract Testing in DApps

At Qxf2, recently we began exploring blockchain technology to understand its potential applications and benefits in various industries. Our journey into testing DApps started with research and self-learning. We immersed ourselves in learning about Ethereum, the leading platform for developing DApps, and its ecosystem of tools and frameworks. Understanding the nuances of Solidity, the programming language for smart contracts, was crucial for effectively testing DApps.

Note: You can play along with this post even if you know nothing about blockchain. We have written a simple app to help you get some hands-on experience with testing DApps.

Smart contracts are a fundamental building block of decentralized applications (DApps) running on blockchain technology. These self-executing contracts contain the rules and logic governing DApps, ensuring transparency and immutability. As smart contracts are crucial financial transactions and sensitive data, ensuring their correctness and security is of utmost importance. As a tester, understanding how to effectively test smart contracts and DApps is crucial to ensure their functionality, security, and overall quality. In this blog post, we will explore the importance of testing smart contracts and how to set up a testing environment using popular tools like Truffleand Ganache. We will also explore a smart contract called “HealthRecord.sol” and delve into its comprehensive test suite.

Code can be found here

Setting up the Environment

You might be wondering how we’ll run these tests since smart contracts are usually executed on the blockchain. Using the actual Ethereum network for testing would be expensive, and testnets, although free, can be bit slow. To overcome these challenges, we’ll utilize a local blockchain. It’s like a lightweight version of the real Ethereum network that runs on your computer and doesn’t require an internet connection.

To begin testing, we used Truffle and Ganache, popular tools for smart contract development and testing. By setting up Ganache and Truffle, you can create a local testing environment for your smart contract development and testing. This makes testing simpler and faster.

Truffle provides a development environment, testing framework for DApps.
Ganache serves as a local blockchain network for testing purposes. The installation steps are listed in readme.md

We are testing our smart contract on the local network and once after completing all our testing, we have deployed our smart contract to Testnet Sepolia. Sepolia ETH can be obtained from a Sepolia testnet faucet, that allows anyone to send a small amount of fake Sepolia ETH to their wallet.

Our Patient Record DApp

We built a small DApp around our smart contract to explore the potential of decentralized applications. Using Truffle React Box we quickly set up the front-end. This DApp, for every patient addition creates a transaction in the network. While testing this DApp, we realized the criticality of smart contract testing and thorough testing was essential to ensure the reliability, security, and integrity of DApps. With this understanding, we shifted our focus from the user interface to the testing of smart contracts.

You can find our DApp code here. You can refer our Readme.md file on how to start the application. In the next sections, we will be focusing on testing the smart contracts.

Patient Health Record DApp
Patient Health Record DApp

About the HealthRecord Smart Contract

The HealthRecord.sol smart contract is designed to manage health records on a blockchain network. The HealthRecord smart contract provides basic functionalities for managing patient records, including adding, updating, and deleting records, while emitting events to track these actions on the blockchain. In the ‘contracts’ folder of the project directory, you can find ‘HealthRecord.sol’ contract.

The contract includes three core functions:

addPatient: This function adds a new patient record to the mapping, but it checks if a patient with the same ID already exists. It also ensures that the name provided is non-empty. We use the require statement to enforce these two conditions.

So if an empty name is provided, the transaction will be reverted, and an error message “Name must not be empty” will be displayed. Similarly, If a patient with the same ID already exists, the transaction will be reverted, and an error message “Patient with this ID already exists” will be displayed. If both the conditions are met, PatientAdded event is emitted using the emit statement.

event PatientAdded(uint256 indexed id, string name, uint256 age);
function addPatient(uint256 id, string memory name, uint256 age) public returns (bytes32) {
        require(patients[id].id != id, "Patient with the given ID already exists");
        require(bytes(name).length > 0, "Name field is required");     
        patients[id] = Patient(id, name, age);       
        emit PatientAdded(id, name, age);        
    }

updatePatient: This function updates an existing patient record based on the oldId. It validates the newName using `require` statement.
Retrives patient record associated with `oldId` from `patients` mapping and stores it. Updates the patient details by assigning the `newID, newName and newAge.
Finally, emits the PatientUpdated event, passing the newId, newName, and newAge as event parameters. This event notifies external applications about the successful update to the patient’s details.

event PatientUpdated(uint256 indexed id, string name, uint256 age);
function updatePatient(uint256 oldId, uint256 newId, string memory newName, uint256 newAge) public returns (bytes32) {
        require(bytes(newName).length > 0, "Name field is required");       
        Patient storage patient = patients[oldId];        
        patient.id = newId;
        patient.name = newName;
        patient.age = newAge;        
        emit PatientUpdated(newId, newName, newAge);        
    }

deletePatient: This function deletes the patient record associated with the given id from the patients mapping using the delete keyword. It emits PatientDeleted event passing the `id` as event parameter. This event notifies external applications about the successful deletion of the patient record.

 event PatientDeleted(uint256 indexed id);
 function deletePatient(uint256 id) public {        
        delete patients[id];        
        emit PatientDeleted(id);
    }

In the following section we will explore different test scenarios which we wrote to test this Smart contract.

Test Suite Overview

Testing a smart contract is essential to ensure its functionality, reliability, and security. In the case of the above HealthRecord.sol smart contract, the below provided tests cover different scenarios and functionalities to validate its behavior. Let’s discuss the testing approach and the purpose of each test. Code can be found here

Truffle test framework gives us two options for writing tests for Smart contracts. Solidity tests and JavaScript tests. We used JavaScript for writing tests.

Truffle uses the Mocha testing framework and Chai for assertions for writing JavaScript tests. To begin with, the code sets up the necessary environment and initializes variables for testing the functionalities of the HealthRecord contract.

const HealthRecord = artifacts.require('HealthRecord');
contract('HealthRecord', (accounts) => {
    let healthRecordInstance;
    const addedPatients = [];
 
    beforeEach(async () => {
        healthRecordInstance = await HealthRecord.deployed();       
    });

The above code in the test does the following:

  • The HealthRecord contract artifact is imported, allowing interaction with the deployed contract.
  • The healthRecordInstance variable is declared to hold an instance of the HealthRecord contract.
  • A “before each” hook is used to assign the deployed instance of the HealthRecord contract to the healthRecordInstance variable, facilitating easy access to the contract throughout the tests.
  • Grouping TestCases

    When a smart contract has a significant number of unit tests, it becomes challenging to read and maintain. Truffle’s integration with Mocha allows for a more organized and readable testing approach using nested describe() functions. By leveraging nested describe() functions, we grouped related test cases together, creating a more structured and organized test suite as discussed below:

    Basic Functionality:
    These tests cover the basic functionality of adding and retrieving patient records in the HealthRecord contract. They validate that patient records are added correctly and can be retrieved accurately, providing a strong foundation for the core functionality of the contract.

    Error Handling:
    Validates how the contract handles exceptional cases, such as adding a patient record with an empty name field.
    Verifies that appropriate errors are thrown when attempting to add duplicate patient IDs.

    Event Emission:
    Verifies the emission of the PatientAdded event when a patient record is added successfully. Ensures the contract emits the expected event and event parameters are accurate.

    Edge Cases:
    Tests scenarios where non-existing patient records are retrieved, ensuring default values are returned.
    Validates the ability of the contract to handle and retrieve multiple patient records accurately.

    Data Integrity:
    Checks if the contract maintains data integrity during record addition, modification, and retrieval.
    Verifies that the stored values accurately reflect the intended changes.

    contract("HealthRecord TestCases", async accounts => {
      describe("Basic Functionality", () => {
        it("should add a patient record", async() => {
          ...
        });
        it("should retrieve a patient record for a non-existing ID", async() => {
          ...
        });
        it("should update an existing patient record", async() => {
          ...
        });
        it("should handle retrieving a patient record from a different account", async() => {
          ...
        });
      });
     
     describe("Error Handling", () => {
        it("should handle empty fields during record addition", async() => {
          ...
        });
        it("should not add duplicate patient ID", async() => {
          ...
        });
      });
     
     describe("Event Emission", () => {
            it("should emit PatientAdded event", async () => {
          ...
        });
      });
     
     describe("Data Consistency", () => {
        it("should update patient age correctly", async() => {
          ...
        });

    The test can be found here. You can execute the test like this.

    `truffle test`

    And the output looks like this:

    Truffle Test output
    Truffle Test output

    Keywords: require, events

    To confirm more thoroughly whether a smart contract behaves as expected or not, smart contract tests should verify fired events. The error messages from require statements are captured in the test cases using try/catch. We use the assert statement to verify that the error message matches the expected error. we can assert and validate the correctness of the error messages thrown by the smart contract when specific conditions are not met. This approach allows us to effectively test the error handling behavior of the contract and ensure that it responds appropriately to exceptional cases.


    We covered various test scenarios, including testing basic functionalities, handling errors, verifying event emissions, and checking data consistency. Each test case played a critical role in validating the correctness of the HealthRecord smart contract, providing users with confidence in its performance and reliability. You can explore more advanced techniques to help identify bugs and vulnerabilities in contract logic. Smart Testing !!!


    References

    1. Create a DApp using Truffle and React
    2. Few tips for unit testing smart contracts
    3. How to test dapps
    4. Etherium DApp Tutorial
    5. Web3.js documentation


    Outsource your testing to Qxf2!

    Choose Qxf2 to elevate your software testing capabilities with our team of technical testers. We excel in testing early-stage products powered by cutting-edge technology stacks. Our expertise in going beyond conventional test automation empowers us to tackle intricate testing challenges, ensuring your product meets the highest quality standards. With Qxf2, you’ll have a dedicated partner who can help you iterate quickly, identify critical bugs, and optimize your development processes for seamless delivery.


    Leave a Reply

    Your email address will not be published. Required fields are marked *