Testing Smart Contracts
Why Smart Contract Testing Is Different
In traditional software, bugs are annoying. In smart contracts, bugs are catastrophic. A deployed contract is immutable โ once it's on Ethereum, the code cannot be changed. If someone finds a vulnerability, they can drain the contract's entire balance before anyone can react.
There is no rollback. There is no hotfix. There is no "we'll patch it in the next release."
This is why smart contract testing is not optional. It is the primary line of defense.
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 lets you write tests in Solidity itself. This is faster (no JavaScript overhead) and gives you access to powerful features like fuzz testing and cheatcodes.
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
- Smart contract bugs are permanent and expensive. Testing is the first and most important layer of security.
- 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 is testing critical for smart contracts specifically?