This chapter shifts the focus to ensuring the quality and reliability of your smart contracts through unit testing. It covers the following key aspects:
Table of Contents
- Setting up Mocha and Chai: Mocha is a popular JavaScript take a look at framework, providing the structure for writing and organizing your checks. Chai is an announcement library that offers helpful capabilities for checking in case your code behaves as predicted (e.G., putting forward that a variable has a positive fee). This segment explains the way to integrate those tools into your Hardhat task.
- Writing Unit Tests for Solidity Contracts: This is the core of the chapter. It teaches you how to write tests specifically for your Solidity smart contracts. You’ll learn how to interact with your contracts from your JavaScript test files, call their functions, and check if the results match your expectations. This often involves using a library like ethers.js to interact with the deployed contract in the test environment.
- Testing Events,
require
Statements, and Edge Cases: A good test suite covers various aspects of your contract’s behavior. This section emphasizes the importance of testing:- Events: Verifying that your contract emits the correct events when certain actions occur. Events are crucial for logging and off-chain monitoring.
require
Statements: Testing that your contract correctly handles error conditions and enforces constraints usingrequire
statements. This ensures that your contract behaves predictably and securely.- Edge Cases: Testing extreme or unusual inputs to ensure your contract doesn’t break under unexpected circumstances. This includes things like very large numbers, zero values, or boundary conditions.
- Running Tests (
npx hardhat test
): Finally, the chapter explains how to execute your tests using thenpx hardhat test
command. Hardhat will run your test files, and you’ll see the results, indicating whether your tests passed or failed. This affords instant comments at the correctness of your code
1. Setting Up Mocha and Chai
Hardhat makes use of Mocha as the take a look at framework and Chai for assertions. These come pre-set up with Hardhat, however in case you need to put in them manually, use:
npm install --save-dev mocha chai chai-as-promised
Test File Structure
By convention, tests are placed inside a /test
directory. Example structure:
/my-hardhat-project
├── contracts/
├── test/
│ ├── Voting.test.js
├── scripts/
├── hardhat.config.js
2. Writing Unit Tests for Solidity Contracts
Hardhat tests are written in JavaScript or TypeScript using ethers.js
.
Example Contract (Voting.sol
)
Let’s write tests for this simple Voting smart contract:
// contracts/Voting.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Voting {
mapping(address => bool) public hasVoted;
uint256 public totalVotes;
event VoteCasted(address indexed voter);
function castVote() external {
require(!hasVoted[msg.sender], "Already voted");
hasVoted[msg.sender] = true;
totalVotes++;
emit VoteCasted(msg.sender);
}
}
3. Writing Unit Tests
Now, let’s test the Voting.sol
contract in test/Voting.test.js
.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Voting Contract", function () {
let Voting, voting, owner, addr1, addr2;
beforeEach(async function () {
// Deploy contract before each test
Voting = await ethers.getContractFactory("Voting");
[owner, addr1, addr2] = await ethers.getSigners();
voting = await Voting.deploy();
await voting.deployed();
});
it("Should initialize totalVotes as zero", async function () {
expect(await voting.totalVotes()).to.equal(0);
});
it("Should allow a user to cast a vote", async function () {
await voting.connect(addr1).castVote();
expect(await voting.hasVoted(addr1.address)).to.equal(true);
expect(await voting.totalVotes()).to.equal(1);
});
it("Should emit a VoteCasted event", async function () {
await expect(voting.connect(addr1).castVote())
.to.emit(voting, "VoteCasted")
.withArgs(addr1.address);
});
it("Should prevent a user from voting twice", async function () {
await voting.connect(addr1).castVote();
await expect(voting.connect(addr1).castVote()).to.be.revertedWith(
"Already voted"
);
});
it("Should allow multiple users to vote", async function () {
await voting.connect(addr1).castVote();
await voting.connect(addr2).castVote();
expect(await voting.totalVotes()).to.equal(2);
});
});
4. Running the Tests
To execute the tests, use:
npx hardhat test
Example Output
Voting Contract
✓ Should initialize totalVotes as zero
✓ Should allow a user to cast a vote
✓ Should emit a VoteCasted event
✓ Should prevent a user from voting twice
✓ Should allow multiple users to vote
5 passing (2s)
5. Testing Edge Cases
Edge cases to consider:
Ensure only unique votes are counted.
Ensure events are emitted correctly.
Ensure require statements prevent invalid actions.
FAQs
1. Why should we write unit tests for Solidity contracts?
Unit tests ensure smart contract logic is correct and prevent costly on-chain errors.
2. How do I check for require
reverts in Hardhat tests?
Use expect().to.be.revertedWith()
:
await expect(voting.connect(addr1).castVote()).to.be.revertedWith("Already voted");
3. How do I simulate a contract interaction from multiple users?
Use connect(signer)
, like this:
await voting.connect(addr1).castVote();
await voting.connect(addr2).castVote();
4. How do I run a specific test?
Use:
npx hardhat test test/Voting.test.js
Or for a single test:
it.only("Should allow a user to vote", async function () { ... });
5. How do I measure gas usage in tests?
Use:
const tx = await voting.connect(addr1).castVote();
const receipt = await tx.wait();
console.log("Gas Used:", receipt.gasUsed.toString());