Unit Testing¶
Unit testing ensures functional correctness of smart contracts through comprehensive test coverage using the Hardhat testing framework.
What Unit Testing Tests For¶
Unit tests validate:
- Functional Correctness: Each contract method behaves as specified
- Edge Cases: Boundary conditions and unusual inputs are handled properly
- Access Control: Only authorized users can call restricted functions
- State Management: Contract state updates correctly after operations
- Event Emissions: Events are emitted with correct parameters
- Revert Conditions: Functions revert with appropriate error messages
- Gas Efficiency: Operations use reasonable amounts of gas
Test Coverage¶
All contracts in the repository have comprehensive unit tests:
| Contract | Test File | Tests |
|---|---|---|
| ConditionalMarketFactory | ConditionalMarketFactory.test.js |
12 tests |
| DAOFactory | DAOFactory.test.js |
24 tests (skipped*) |
| FutarchyGovernor | FutarchyGovernor.test.js |
13 tests |
| OracleResolver | OracleResolver.test.js |
13 tests |
| PrivacyCoordinator | PrivacyCoordinator.test.js |
13 tests |
| ProposalRegistry | ProposalRegistry.test.js |
25 tests |
| RagequitModule | RagequitModule.test.js |
12 tests |
| WelfareMetricRegistry | WelfareMetricRegistry.test.js |
17 tests |
Total: 95 passing tests
* DAOFactory tests are skipped due to the contract exceeding EIP-170's 24KB bytecode size limit. The contract deploys 6 sub-contracts in the constructor, causing it to exceed the deployment size limit. This is a known issue that requires contract refactoring (e.g., using EIP-1167 minimal proxies).
Running Unit Tests¶
All Tests¶
Specific Test File¶
With Gas Reporting¶
This generates a detailed gas usage report showing: - Gas used for contract deployment - Gas used for each method call - Average, minimum, and maximum gas consumption - USD cost estimates (when API key provided)
With Coverage¶
Generates HTML coverage reports showing: - Line coverage - Branch coverage - Statement coverage - Function coverage
Coverage reports are saved to coverage/ directory.
Coverage Goals¶
Target coverage metrics:
- Statements: > 80%
- Branches: > 75%
- Functions: > 90%
- Lines: > 80%
Test Structure¶
Tests follow a consistent structure:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("ContractName", function () {
let contract;
let owner;
let addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("ContractName");
contract = await Contract.deploy();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await contract.owner()).to.equal(owner.address);
});
});
describe("FunctionName", function () {
it("Should perform expected action", async function () {
await expect(contract.functionName())
.to.emit(contract, "EventName");
});
it("Should revert on invalid input", async function () {
await expect(contract.functionName(invalidInput))
.to.be.revertedWith("Error message");
});
});
});
Helper Contracts¶
Mock contracts are available for testing:
- MockERC20: ERC20 token for testing token interactions
- Located in
contracts/mocks/MockERC20.sol - Includes
mintandburnfunctions for flexible testing
CI/CD Integration¶
Unit tests run automatically in the GitHub Actions workflow:
Job: hardhat-tests
- Runs on every pull request and push
- Uploads gas report as artifact
- Fails the build if any test fails
Best Practices¶
When writing unit tests:
- Test one thing at a time: Each test should verify a single behavior
- Use descriptive names: Test names should clearly state what they test
- Test both success and failure cases: Cover happy paths and error conditions
- Use beforeEach for setup: Keep tests independent and repeatable
- Check events and state changes: Verify complete contract behavior
- Use helper functions: Create utilities for common test operations
- Test access control: Verify permissions are enforced correctly
- Test edge cases: Check boundary conditions and unusual inputs
Gas Optimization¶
Monitor gas usage with the gas reporter:
Look for: - High gas usage: Operations using > 300,000 gas - Inconsistent costs: Wide variation between min/max values - Optimization opportunities: Storage operations, loops, string manipulation
Example Test¶
describe("ProposalRegistry", function () {
describe("Proposal Submission", function () {
it("Should allow submission with correct bond", async function () {
const BOND_AMOUNT = ethers.parseEther("50");
await expect(
proposalRegistry.connect(proposer).submitProposal(
"Test Proposal",
"Description",
ethers.parseEther("1000"),
recipient.address,
0, // welfareMetricId
ethers.ZeroAddress, // native token
0, // startDate
getFutureTimestamp(90), // deadline
{ value: BOND_AMOUNT }
)
).to.emit(proposalRegistry, "ProposalSubmitted");
});
it("Should reject submission with incorrect bond", async function () {
await expect(
proposalRegistry.connect(proposer).submitProposal(
"Test Proposal",
"Description",
ethers.parseEther("1000"),
recipient.address,
0,
ethers.ZeroAddress,
0,
getFutureTimestamp(90),
{ value: ethers.parseEther("10") } // Too low
)
).to.be.revertedWith("Insufficient bond");
});
});
});
Troubleshooting¶
Tests Fail Locally But Pass in CI¶
- Ensure Node.js version matches CI (v20)
- Clear Hardhat cache:
npm run clean - Reinstall dependencies:
rm -rf node_modules && npm install
Timeout Errors¶
- Increase timeout in test:
this.timeout(60000) - Check for stuck async operations
- Verify network connection for tests requiring external services
Gas Estimation Errors¶
- Ensure sufficient balance in test accounts
- Check for revert conditions in the transaction
- Verify contract state before transaction