DEV Community

Cover image for Writing Unit Tests for CrossFi DApp with Mocha & Chai
Azeez Abidoye
Azeez Abidoye

Posted on

2 1 1 1 2

Writing Unit Tests for CrossFi DApp with Mocha & Chai

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
Enter fullscreen mode Exit fullscreen mode

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 chai@4.3.7 --dev
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
  });
Enter fullscreen mode Exit fullscreen mode
  • 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;
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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;
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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;
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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"));
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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);
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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!"
    );
  });
Enter fullscreen mode Exit fullscreen mode

✅ 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!"
    );
  });
});
Enter fullscreen mode Exit fullscreen mode
  • To run the tests, execute the following command.
yarn hardhat test
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode

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

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (1)

Collapse
 
adejare02 profile image
KENNY ADEJARE

Very helpful

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay