Overview

In the previous article, Developing a Rental DApp on CrossFi using Hardhat, we explored the fundamental steps for developing a decentralized rental application covering:

✅ Smart Contract Development: Writing a Solidity smart contract to manage the rental process.

✅ Generating a CrossFi API Key on Alchemy: Setting up Alchemy to interact with the CrossFi blockchain.

✅ Deploying the Contract: Using Hardhat to compile, and successfully deploy the contract on CrossFi chain.

Building on that foundation, this tutorial, focuses on ensuring the reliability of our smart contract through unit testing. We'll explore how to set up a test environment with Mocha and Chai, write comprehensive test cases, and validate the contract's functionality before deploying it to production.

⚠️ Recommended: This tutorial provides a fundamental understanding of the core concepts required in developing a decentralized application on the CrossFi testnet.

Dev Tool 🛠️

  • Yarn
npm install -g yarn

Writing unit tests with Mocha and Chai

Unit testing is essential for verifying the functionality of any component, whether it’s a class, a function, or a single line of code.

In this piece, we will explore how to unit test Solidity smart contracts using Mocha, a lightweight Node.js framework, and Chai, a Test-Driven Development (TDD) assertion library for Node.js. Both Mocha and Chai run on Node.js and support asynchronous testing in the browser. While Mocha can be used with various assertion libraries, it is most commonly paired with Chai for its flexibility and readability.

First, make sure you have the required dependencies installed.

yarn add mocha [email protected] --dev

Create a new file for the Test Script

  • Navigate to the test directory
  • Create a new file named rental-test.cjs

Understanding the Test Script

The test script is written using Mocha (a JavaScript test framework) and Chai (an assertion library) along with Hardhat's ethers.js for interacting with the smart contract.

1. Setting Up the Test Environment

const { expect } = require("chai");
const { ethers } = require("hardhat");
  • Chai provides the expect function for writing assertions.
  • Ethers.js is used to deploy and interact with the smart contract.
describe("Rental Smart Contract", function () {
  let Rental, rental, owner, renter;
}
  • describe(...) is Mocha’s way of grouping test cases. We declare variables:
  • Rental: The contract factory.
  • rental: The deployed contract instance.
  • owner: The deployer of the contract.
  • renter: A second account that acts as a renter.

2. Deploying the Contract Before Each Test

beforeEach(async function () {
    Rental = await ethers.getContractFactory("Rental");
    [owner, renter] = await ethers.getSigners();
    rental = await Rental.deploy();
    await rental.waitForDeployment();
  });
  • beforeEach(...) ensures the contract is freshly deployed before every test.
  • getSigners() gives us two Ethereum accounts (owner and renter).
  • deploy() deploys the Rental contract.

Test Cases Explained

1. Adding a Renter

it("Should add a renter successfully", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    const addedRenter = await rental.renters(renter.address);
    expect(addedRenter.firstName).to.equal("John");
    expect(addedRenter.canRent).to.be.true;
  });

✅ What it does:

  • Calls addRenter(...) to add a new renter.
  • Fetches renter data from the contract and checks:
  • First name should be "John".
  • canRent should be true.

2. Checking Out a Car

it("Should allow renter to check out a car", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    await rental.checkOut(renter.address);
    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.active).to.be.true;
    expect(updatedRenter.canRent).to.be.false;
  });

✅ What it does:

  • Adds a renter.
  • Calls checkOut(...) for that renter.
  • Checks if:
  • active is true (renter has an active rental).
  • canRent is false (renter cannot rent another car).

3. Checking In a Car

it("Should allow renter to check in a car", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    await rental.checkOut(renter.address);
    await rental.checkIn(renter.address);

    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.active).to.be.false;
  });

✅ What it does:

  • Adds a renter.
  • Calls checkOut(...) → renter takes a car.
  • Calls checkIn(...) → renter returns the car.
  • Checks if:
  • active is false (rental session ended).

4. Depositing Funds

it("Should allow renter to deposit funds", async function () {
    await rental.deposit(renter.address, { value: ethers.parseEther("1") });
    const renterBalance = await rental.balanceOfRenter(renter.address);
    expect(renterBalance).to.equal(ethers.parseEther("1"));
  });

✅ What it does:

  • Calls deposit(...) to add 1 ETH to the renter’s balance.
  • Verifies if the renter’s balance is exactly 1 ETH.

5. Making a Payment After Check-In

it("Should allow renter to make payment and reset status", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      ethers.parseEther("0.05"),
      0,
      0
    );

    await rental.deposit(renter.address, { value: ethers.parseEther("1") });
    await rental.makePayment(renter.address, {
      value: ethers.parseEther("0.05"),
    });

    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.canRent).to.be.true;
    expect(updatedRenter.amountDue).to.equal(0);
  });

✅ What it does:

  • Adds a renter and sets an amountDue of 0.05 ETH.
  • Deposits 1 ETH to the renter’s balance.
  • Calls makePayment(...) to pay the 0.05 ETH.
  • Checks if:
  • canRent is true (renter can rent again).
  • amountDue is 0 (payment cleared).

6. Preventing Checkout with Pending Balance

it("Should not allow renter to check out with pending balance", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      ethers.parseEther("0.05"),
      0,
      0
    );

    await expect(rental.checkOut(renter.address)).to.be.revertedWith(
      "You have a pending balance!"
    );
  });

✅ What it does:

  • Adds a renter who already owes 0.05 ETH.
  • Calls checkOut(...).
  • Expects the transaction to fail with the message "You have a pending balance!".

Updating the Test Script

  • This is how rental-test.cjs would look once all the test cases have been included.
const { ethers } = require("hardhat");
const { expect } = require("chai");

describe("Rental Smart Contract", function () {
  let Rental, rental, owner, renter;

  beforeEach(async function () {
    Rental = await ethers.getContractFactory("Rental");
    [owner, renter] = await ethers.getSigners();
    rental = await Rental.deploy();
    await rental.waitForDeployment();
  });

  it("Should add a renter successfully", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    const addedRenter = await rental.renters(renter.address);
    expect(addedRenter.firstName).to.equal("John");
    expect(addedRenter.canRent).to.be.true;
  });

  it("Should allow renter to check out a car", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    await rental.checkOut(renter.address);
    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.active).to.be.true;
    expect(updatedRenter.canRent).to.be.false;
  });

  it("Should allow renter to check in a car", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      0,
      0,
      0
    );

    await rental.checkOut(renter.address);
    await rental.checkIn(renter.address);

    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.active).to.be.false;
  });

  it("Should allow renter to deposit funds", async function () {
    await rental.deposit(renter.address, { value: ethers.parseEther("1") });
    const renterBalance = await rental.balanceOfRenter(renter.address);
    expect(renterBalance).to.equal(ethers.parseEther("1"));
  });

  it("Should allow renter to make payment and reset status", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      ethers.parseEther("0.05"),
      0,
      0
    );

    await rental.deposit(renter.address, { value: ethers.parseEther("1") });
    await rental.makePayment(renter.address, {
      value: ethers.parseEther("0.05"),
    });

    const updatedRenter = await rental.renters(renter.address);
    expect(updatedRenter.canRent).to.be.true;
    expect(updatedRenter.amountDue).to.equal(0);
  });

  it("Should not allow renter to check out with pending balance", async function () {
    await rental.addRenter(
      renter.address,
      "John",
      "Doe",
      true,
      false,
      ethers.parseEther("1"),
      ethers.parseEther("0.05"),
      0,
      0
    );

    await expect(rental.checkOut(renter.address)).to.be.revertedWith(
      "You have a pending balance!"
    );
  });
});
  • To run the tests, execute the following command.
yarn hardhat test
  • Your test should pass with the following results:
Rental Smart Contract
✔ Should add a renter successfully
✔ Should allow renter to check out a car
✔ Should allow renter to check in a car
✔ Should allow renter to deposit funds
✔ Should allow renter to make payment and reset status
✔ Should not allow renter to check out with pending balance

6 passing (429ms)

✨ Done in 2.33s.

Conclusion

This test suite verifies the core functionalities of the smart contract:
✅ Adding renters
✅ Checking out cars
✅ Checking in cars
✅ Depositing funds
✅ Making payments
✅ Preventing invalid actions