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 chai@4.3.7 --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
andrenter
). -
deploy()
deploys theRental
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 betrue
.
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
istrue
(renter has an active rental). -
canRent
isfalse
(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
isfalse
(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
istrue
(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
Top comments (1)
Very helpful