Smart Contract Testing
SkillDev toolsGuide for testing smart contracts using Foundry and Hardhat, covering unit tests, fuzz testing, invariant testing, fork testing, and gas benchmarking.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Smart Contract Testing skill
What this skill tells your AI
The instructions your AI receives, as published by nirholas/three.ws in data/skills/development/smart-contract-testing/SKILL.md and read by ahel’s review.
When to use this skill
Use when the user asks about:
- Writing tests for smart contracts
- Setting up a testing framework (Foundry or Hardhat)
- Fuzz testing or invariant testing strategies
- Fork testing against mainnet state
- Gas benchmarking and optimization verification
- Debugging failing contract tests
Testing Framework Selection
Foundry vs Hardhat
| Feature | Foundry | Hardhat |
|---|---|---|
| Language | Solidity | JavaScript/TypeScript |
| Speed | Very fast (native) | Slower (JS runtime) |
| Fuzzing | Built-in | Requires plugins |
| Fork testing | Built-in | Built-in |
| Debugging | forge debug, traces | console.sol, traces |
| Ecosystem | Growing rapidly | Mature, large plugin ecosystem |
Recommendation: Use Foundry for new projects. Use Hardhat when you need complex JS scripting or specific Hardhat plugins.
Testing with Foundry
1. Unit Test Structure
Follow this pattern for each test:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {MyContract} from "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract internal target;
address internal alice = makeAddr("alice");
address internal bob = makeAddr("bob");
function setUp() public {
target = new MyContract();
deal(alice, 100 ether);
}
function test_deposit_succeeds() public {
vm.prank(alice);
target.deposit{value: 1 ether}();
assertEq(target.balanceOf(alice), 1 ether);
}
function test_deposit_revertsWhenZero() public {
vm.prank(alice);
vm.expectRevert(MyContract.ZeroAmount.selector);
target.deposit{value: 0}();
}
}
Naming conventions:
test_functionName_scenariofor expected-pass teststest_functionName_revertsWhen_conditionfor expected-revert teststestFuzz_functionNamefor fuzz tests
2. Fuzz Testing
Let Foundry generate random inputs to find edge cases:
function testFuzz_deposit(uint256 amount) public {
// Bound the input to realistic ranges
amount = bound(amount, 1, 100 ether);
deal(alice, amount);
vm.prank(alice);
target.deposit{value: amount}();
assertEq(target.balanceOf(alice), amount);
}
Best practices for fuzz tests:
- Use
bound()to constrain inputs to valid ranges - Use
vm.assume()sparingly — preferbound()to avoid too many rejected runs - Set meaningful fuzz runs in
foundry.toml:[fuzz] runs = 1000 - Focus fuzz testing on functions with numerical inputs and complex math
3. Invariant Testing
Define properties that must always hold regardless of actions:
contract MyContractInvariant is Test {
MyContract internal target;
Handler internal handler;
function setUp() public {
target = new MyContract();
handler = new Handler(target);
targetContract(address(handler));
}
function invariant_totalSupplyMatchesBalances() public view {
assertEq(
target.totalSupply(),
target.balanceOf(address(handler))
);
}
}
Common invariants to test:
- Total supply equals sum of all balances
- Contract balance always covers total deposits
- Exchange rates are monotonically non-decreasing
- Access control roles cannot be self-granted
4. Fork Testing
Test against real mainnet state:
function test_swapOnUniswap_fork() public {
// Fork mainnet at a specific block
vm.createSelectFork("mainnet", 19_000_000);
address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
deal(usdc, alice, 10_000e6);
vm.startPrank(alice);
// ... execute swap logic
vm.stopPrank();
assertGt(IERC20(weth).balanceOf(alice), 0);
}
Fork testing tips:
- Pin to a specific block number for reproducibility
- Use
deal()to set up token balances without needing to acquire them - Test against multiple chains by configuring RPC URLs in
foundry.toml - Be mindful of rate limits on RPC providers
5. Gas Benchmarking
Track gas usage to catch regressions:
# Generate gas snapshot
forge snapshot
# Compare against previous snapshot
forge snapshot --check
In tests, measure specific operations:
function test_deposit_gasUsage() public {
vm.prank(alice);
uint256 gasBefore = gasleft();
target.deposit{value: 1 ether}();
uint256 gasUsed = gasBefore - gasleft();
// Assert gas is within expected range
assertLt(gasUsed, 50_000, "deposit uses too much gas");
}
6. Coverage and CI
Ensure comprehensive test coverage:
- Run
forge coverageto identify uncovered code paths - Target 100% line coverage on security-critical functions
- Set up CI to run tests on every PR
- Include gas snapshot comparison in CI to catch regressions
- Run fuzz tests with higher iterations in CI (10,000+) vs local development (256)
7. Debugging Techniques
When tests fail:
- Use
forge test -vvvvfor full execution traces - Add
console.log()viaimport "forge-std/console.sol"; - Use
forge debugfor step-by-step EVM execution - Check event emissions with
vm.expectEmit() - Verify storage values with
vm.load(address, slot)
Signals
- GitHub stars
- 114
- Forks
- 29
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
smart-contract-testing- Source
- github.com/nirholas/three.ws