Testing Smart Contracts
Why Smart Contract Testing Is Different
Tests check a contract's behavior before and after changes. Include permissions, accounting, external calls, and failure cases, especially where a defect could affect user assets.
Recovery after deployment depends on the contract's design and permissions. An upgrade or pause mechanism is itself something to test.
Use tests alongside code review, deployment checks, and monitoring.
Testing with Hardhat (JavaScript)
Hardhat is the most popular development framework for Ethereum. Tests are written in JavaScript or TypeScript using Mocha and Chai.
A Simple Token Test
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleToken", function () {
let token, owner, alice;
beforeEach(async function () {
[owner, alice] = await ethers.getSigners();
const Token = await ethers.getContractFactory("SimpleToken");
token = await Token.deploy(1000); // mint 1000 tokens to deployer
});
it("should assign total supply to the owner", async function () {
const balance = await token.balanceOf(owner.address);
expect(balance).to.equal(1000);
});
it("should transfer tokens between accounts", async function () {
await token.transfer(alice.address, 100);
expect(await token.balanceOf(alice.address)).to.equal(100);
expect(await token.balanceOf(owner.address)).to.equal(900);
});
it("should revert when sender has insufficient balance", async function () {
await expect(
token.connect(alice).transfer(owner.address, 1)
).to.be.revertedWith("Insufficient balance");
});
});
What this tests:
- Initial state is correct (deployer gets all tokens).
- The core function works (transfer moves tokens).
- Error handling works (transferring more than you have reverts).
Run with: npx hardhat test
Testing with Foundry (Solidity)
Foundry supports Solidity tests, generated-input testing, and controls for simulating accounts, time, and other execution conditions. Runtime performance depends on the test suite and configuration.
The Same Test in Foundry
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SimpleToken.sol";
contract SimpleTokenTest is Test {
SimpleToken token;
address alice = makeAddr("alice");
function setUp() public {
token = new SimpleToken(1000);
}
function test_OwnerGetsInitialSupply() public view {
assertEq(token.balanceOf(address(this)), 1000);
}
function test_Transfer() public {
token.transfer(alice, 100);
assertEq(token.balanceOf(alice), 100);
assertEq(token.balanceOf(address(this)), 900);
}
function test_RevertOnInsufficientBalance() public {
vm.prank(alice); // next call comes from alice
vm.expectRevert("Insufficient balance");
token.transfer(address(this), 1);
}
}
Run with: forge test
Fuzz Testing
Fuzz testing is where Foundry excels. Instead of testing with hardcoded values, you let the framework generate random inputs.
function testFuzz_TransferNeverExceedsSupply(uint256 amount) public {
// Bound the amount to something reasonable
amount = bound(amount, 0, token.totalSupply());
token.transfer(alice, amount);
// Invariant: total supply should never change
assertEq(
token.balanceOf(address(this)) + token.balanceOf(alice),
token.totalSupply()
);
}
Foundry will run this function with hundreds of random amount values - including 0, 1, max uint256, and everything in between. If any input breaks the invariant, Foundry reports the exact failing case.
What to Test: A Checklist
| Category | What to verify | Example |
|---|---|---|
| Access control | Only authorized addresses can call admin functions | onlyOwner modifier works; random users get reverted |
| State changes | Functions modify storage correctly | After deposit, balance increases by exact amount |
| Edge cases | Zero amounts, max values, empty arrays | Transferring 0 tokens doesn't break anything |
| Reverts | Invalid operations fail cleanly | Withdrawing more than your balance reverts |
| Reentrancy | External calls don't allow re-entry | Callback during withdrawal can't drain the contract |
| Events | Correct events are emitted | Transfer event fires with correct from, to, amount |
Key takeaways
- Test expected behavior and failure cases before deployment and after every change.
- Hardhat uses JavaScript tests (familiar, good ecosystem). Foundry uses Solidity tests (faster, native fuzz testing).
- Fuzz testing automatically generates random inputs to catch edge cases you would never think of manually.
- Always test access control first - who can call what, and what happens when unauthorized users try.
Quiz: Testing Smart Contracts
1 / 5Why test a contract's failure cases before deployment?