
Testing
- 35 installs
- 244 repo stars
- Updated July 21, 2026
- austintgriffith/ethskills
Testing is a Claude skill for smart-contract testing with Foundry, covering unit, fuzz, fork, and invariant tests.
About
This skill covers smart-contract testing with Foundry, including unit, fuzz, fork, and invariant tests. A developer uses it when writing tests for a Solidity contract. It stresses testing edge cases, economic invariants, and integrations against real deployed protocols rather than trivial getters.
- Smart contract testing with Foundry: unit, fuzz, fork, and invariant tests
- Emphasizes testing edge cases, failure modes, and economic invariants over trivial getters
- Fork-testing against real deployed protocols (Uniswap, Aave) to catch integration bugs
Testing by the numbers
- 35 all-time installs (skills.sh)
- Ranked #1,312 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
testing capabilities & compatibility
- Capabilities
- testing · smart contract testing · code review
- Use cases
- testing · debugging
- Pricing
- Free
What testing says it does
If your contract calls Uniswap, Aave, or any external protocol (verified addresses: `addresses/SKILL.md`), test against their real deployed contracts on a fork.
npx skills add https://github.com/austintgriffith/ethskills --skill testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 244 |
| Last updated | July 21, 2026 |
| Repository | austintgriffith/ethskills ↗ |
What it does
Write Foundry unit, fuzz, fork, and invariant tests that exercise a smart contract's edge cases and economic invariants.
Who is it for?
Solidity developers writing Foundry test suites
When should I use this skill?
writing tests for a smart contract
What you get
A Foundry test suite covering edge cases, fuzzed inputs, forked integrations, and stateful invariants.
By the numbers
- four test types covered: unit, fuzz, fork, invariant
Files
Smart Contract Testing
What You Probably Got Wrong
You test getters and trivial functions. Testing that name() returns the name is worthless. Test edge cases, failure modes, and economic invariants — the things that lose money when they break.
You don't fuzz. forge test finds the bugs you thought of. Fuzzing finds the ones you didn't. If your contract does math, fuzz it. If it handles user input, fuzz it. If it moves value, definitely fuzz it.
You don't fork-test. If your contract calls Uniswap, Aave, or any external protocol (verified addresses: addresses/SKILL.md), test against their real deployed contracts on a fork. Mocking them hides integration bugs that only appear with real state.
You write tests that mirror the implementation. Testing that deposit(100) sets balance[user] = 100 is tautological — you're testing that Solidity assignments work. Test properties: "after deposit and withdraw, user gets their tokens back." Test invariants: "total deposits always equals contract balance."
You skip invariant testing for stateful protocols. If your contract has multiple interacting functions that change state over time (vaults, AMMs, lending), you need invariant tests. Unit tests check one path; invariant tests check that properties hold across thousands of random sequences.
---
Unit Testing with Foundry
Test File Structure
// test/MyContract.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public token;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
token = new MyToken("Test", "TST", 1_000_000e18);
// Give alice some tokens for testing
token.transfer(alice, 10_000e18);
}
function test_TransferUpdatesBalances() public {
vm.prank(alice);
token.transfer(bob, 1_000e18);
assertEq(token.balanceOf(alice), 9_000e18);
assertEq(token.balanceOf(bob), 1_000e18);
}
function test_TransferEmitsEvent() public {
vm.expectEmit(true, true, false, true);
emit Transfer(alice, bob, 500e18);
vm.prank(alice);
token.transfer(bob, 500e18);
}
function test_RevertWhen_TransferExceedsBalance() public {
vm.prank(alice);
vm.expectRevert();
token.transfer(bob, 999_999e18); // More than alice has
}
function test_RevertWhen_TransferToZeroAddress() public {
vm.prank(alice);
vm.expectRevert();
token.transfer(address(0), 100e18);
}
}Key Assertion Patterns
// Equality
assertEq(actual, expected);
assertEq(actual, expected, "descriptive error message");
// Comparisons
assertGt(a, b); // a > b
assertGe(a, b); // a >= b
assertLt(a, b); // a < b
assertLe(a, b); // a <= b
// Approximate equality (for math with rounding)
assertApproxEqAbs(actual, expected, maxDelta);
assertApproxEqRel(actual, expected, maxPercentDelta); // in WAD (1e18 = 100%)
// Revert expectations
vm.expectRevert(); // Any revert
vm.expectRevert("Insufficient balance"); // Specific message
vm.expectRevert(MyContract.CustomError.selector); // Custom error
// Event expectations
vm.expectEmit(true, true, false, true); // (topic1, topic2, topic3, data)
emit MyEvent(expectedArg1, expectedArg2);What to Actually Test
// ✅ TEST: Edge cases that lose money
function test_TransferZeroAmount() public { /* ... */ }
function test_TransferEntireBalance() public { /* ... */ }
function test_TransferToSelf() public { /* ... */ }
function test_ApproveOverwrite() public { /* ... */ }
function test_TransferFromWithExactAllowance() public { /* ... */ }
// ✅ TEST: Access control
function test_RevertWhen_NonOwnerCallsAdminFunction() public { /* ... */ }
function test_OwnerCanPause() public { /* ... */ }
// ✅ TEST: Failure modes
function test_RevertWhen_DepositZero() public { /* ... */ }
function test_RevertWhen_WithdrawMoreThanDeposited() public { /* ... */ }
function test_RevertWhen_ContractPaused() public { /* ... */ }
// ❌ DON'T TEST: OpenZeppelin internals
// function test_NameReturnsName() — they already tested this
// function test_SymbolReturnsSymbol() — waste of time
// function test_DecimalsReturns18() — it does, trust it---
Fuzz Testing
Foundry automatically fuzzes any test function with parameters. Instead of testing one value, it tests hundreds of random values.
Basic Fuzz Test
// Foundry calls this with random amounts
function testFuzz_DepositWithdrawRoundtrip(uint256 amount) public {
// Bound input to valid range
amount = bound(amount, 1, token.balanceOf(alice));
uint256 balanceBefore = token.balanceOf(alice);
vm.startPrank(alice);
token.approve(address(vault), amount);
vault.deposit(amount, alice);
vault.withdraw(vault.balanceOf(alice), alice, alice);
vm.stopPrank();
// Property: user gets back what they deposited (minus any fees)
assertGe(token.balanceOf(alice), balanceBefore - 1); // Allow 1 wei rounding
}Bounding Inputs
// bound() is preferred over vm.assume() — bound reshapes, assume discards
function testFuzz_Fee(uint256 amount, uint256 feeBps) public {
amount = bound(amount, 1e6, 1e30); // Reasonable token amounts
feeBps = bound(feeBps, 1, 10_000); // 0.01% to 100%
uint256 fee = (amount * feeBps) / 10_000;
uint256 afterFee = amount - fee;
// Property: fee + remainder always equals original
assertEq(fee + afterFee, amount);
}
// vm.assume() discards inputs — use sparingly
function testFuzz_Division(uint256 a, uint256 b) public {
vm.assume(b > 0); // Skip zero (would revert)
// ...
}Run with More Iterations
# Default: 256 runs
forge test
# More thorough: 10,000 runs
forge test --fuzz-runs 10000
# Set in foundry.toml for CI
# [fuzz]
# runs = 1000---
Fork Testing
Test your contract against real deployed protocols on a mainnet fork. This catches integration bugs that mocks can't.
Basic Fork Test
contract SwapTest is Test {
// Real mainnet addresses — full verified list: addresses/SKILL.md
address constant UNISWAP_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
// Fork mainnet at a specific block for reproducibility
vm.createSelectFork("mainnet", 19_000_000);
}
function test_SwapETHForUSDC() public {
address user = makeAddr("user");
vm.deal(user, 1 ether);
vm.startPrank(user);
// Build swap path
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
fee: 3000,
recipient: user,
amountIn: 0.1 ether,
amountOutMinimum: 0, // In production, NEVER set to 0
sqrtPriceLimitX96: 0
});
// Execute swap
uint256 amountOut = ISwapRouter(UNISWAP_ROUTER).exactInputSingle{value: 0.1 ether}(params);
vm.stopPrank();
// Verify we got USDC back
assertGt(amountOut, 0, "Should receive USDC");
assertGt(IERC20(USDC).balanceOf(user), 0);
}
}When to Fork-Test
- Always: Any contract that calls an external protocol (Uniswap, Aave, Chainlink)
- Always: Any contract that handles tokens with quirks (USDT, fee-on-transfer, rebasing)
- Always: Any contract that reads oracle prices
- Never: Pure logic contracts with no external calls — use unit tests
Running Fork Tests
# Fork from RPC URL
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# Fork at specific block (reproducible)
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY --fork-block-number 19000000
# Set in foundry.toml to avoid CLI flags
# [rpc_endpoints]
# mainnet = "${MAINNET_RPC_URL}"---
Invariant Testing
Invariant tests verify that properties hold across thousands of random function call sequences. Essential for stateful protocols.
What Are Invariants?
Invariants are properties that must ALWAYS be true, no matter what sequence of actions users take:
- "Total supply equals sum of all balances" (ERC-20)
- "Total deposits equals total shares times share price" (vault)
- "x * y >= k after every swap" (AMM)
- "User can always withdraw what they deposited" (escrow)
Basic Invariant Test
contract VaultInvariantTest is Test {
MyVault public vault;
IERC20 public token;
VaultHandler public handler;
function setUp() public {
token = new MockERC20("Test", "TST", 18);
vault = new MyVault(token);
handler = new VaultHandler(vault, token);
// Tell Foundry which contract to call randomly
targetContract(address(handler));
}
// This runs after every random sequence
function invariant_TotalAssetsMatchesBalance() public view {
assertEq(
vault.totalAssets(),
token.balanceOf(address(vault)),
"Total assets must equal actual balance"
);
}
function invariant_SharePriceNeverZero() public view {
if (vault.totalSupply() > 0) {
assertGt(vault.convertToAssets(1e18), 0, "Share price must never be zero");
}
}
}
// Handler: guided random actions
contract VaultHandler is Test {
MyVault public vault;
IERC20 public token;
constructor(MyVault _vault, IERC20 _token) {
vault = _vault;
token = _token;
}
function deposit(uint256 amount) public {
amount = bound(amount, 1, 1e24);
deal(address(token), msg.sender, amount);
vm.startPrank(msg.sender);
token.approve(address(vault), amount);
vault.deposit(amount, msg.sender);
vm.stopPrank();
}
function withdraw(uint256 shares) public {
uint256 maxShares = vault.balanceOf(msg.sender);
if (maxShares == 0) return;
shares = bound(shares, 1, maxShares);
vm.prank(msg.sender);
vault.redeem(shares, msg.sender, msg.sender);
}
}Running Invariant Tests
# Default depth (15 calls per sequence, 256 sequences)
forge test
# Deeper exploration
forge test --fuzz-runs 1000
# Configure in foundry.toml
# [invariant]
# runs = 512
# depth = 50---
What NOT to Test
- OpenZeppelin internals. Don't test that
ERC20.transferworks. It's been audited by dozens of firms and used by thousands of contracts. Test YOUR logic on top of it. - Solidity language features. Don't test that
requirereverts or thatmappingstores values. The compiler works. - Every getter. If
name()returns the name you passed to the constructor, that's not a test — it's a tautology. - Happy path only. The happy path probably works. Test the unhappy paths: what happens with zero? Max uint? Unauthorized callers? Reentrancy?
Focus your testing effort on: Custom business logic, mathematical operations, integration points with external protocols, access control boundaries, and economic edge cases.
---
Pre-Deploy Test Checklist
- [ ] All custom logic has unit tests with edge cases
- [ ] Zero amounts, max uint, empty arrays, self-transfers tested
- [ ] Access control verified — unauthorized calls revert
- [ ] Fuzz tests on all mathematical operations (minimum 1000 runs)
- [ ] Fork tests for every external protocol integration
- [ ] Invariant tests for stateful protocols (vaults, AMMs, lending)
- [ ] Events verified with
expectEmit - [ ] Gas snapshots taken with
forge snapshotto catch regressions - [ ] Static analysis with
slither .— no high/medium findings unaddressed - [ ] All tests pass:
forge test -vvv
Related skills
FAQ
Why fuzz a smart contract?
forge test finds the bugs you thought of; fuzzing finds the ones you didn't, especially for math, user input, and value transfers.
When do I need invariant tests?
For stateful protocols like vaults, AMMs, and lending where multiple functions interact over time; unit tests check one path, invariants check properties across thousands of sequences.