# Foundry Testing Guide Complete guide to testing smart contracts with Forge. ## Test Structure ### File Conventions - Test files: `ContractName.t.sol` - Script files: `ScriptName.s.sol` - Test contracts inherit from `Test` ### Function Prefixes | Prefix | Purpose | |--------|---------| | `test_` | Unit test | | `testFuzz_` | Fuzz test | | `testFork_` | Fork test | | `invariant_` | Invariant test | | `test_RevertIf_` | Expected revert | | `test_RevertWhen_` | Expected revert | ### Basic Test Contract ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Test, console} from "forge-std/Test.sol"; import {Vault} from "../src/Vault.sol"; contract VaultTest is Test { Vault public vault; address public alice; address public bob; function setUp() public { vault = new Vault(); alice = makeAddr("alice"); bob = makeAddr("bob"); deal(alice, 100 ether); deal(bob, 100 ether); } function test_Deposit() public { vm.prank(alice); vault.deposit{value: 1 ether}(); assertEq(vault.balanceOf(alice), 1 ether); } } ``` ## Unit Testing ### Assertions ```solidity // Equality assertEq(actual, expected); assertEq(actual, expected, "custom message"); assertNotEq(actual, expected); // Comparisons assertGt(a, b); // a > b assertGe(a, b); // a >= b assertLt(a, b); // a < b assertLe(a, b); // a <= b // Boolean assertTrue(condition); assertFalse(condition); // Approximation (for rounding) assertApproxEqAbs(actual, expected, maxDelta); assertApproxEqRel(actual, expected, maxPercentDelta); // 1e18 = 100% // Arrays assertEq(arr1, arr2); ``` ### Testing Reverts ```solidity // Expect any revert vm.expectRevert(); vault.withdraw(1000 ether); // Expect specific message vm.expectRevert("Insufficient balance"); vault.withdraw(1000 ether); // Expect custom error (selector only) vm.expectRevert(InsufficientBalance.selector); vault.withdraw(1000 ether); // Expect custom error with parameters vm.expectRevert( abi.encodeWithSelector( InsufficientBalance.selector, 0, // available 1000 // required ) ); vault.withdraw(1000 ether); // Partial revert (match selector only) vm.expectPartialRevert(InsufficientBalance.selector); vault.withdraw(1000 ether); ``` ### Testing Events ```solidity // Expect event emission vm.expectEmit(true, true, false, true); // topic1, topic2, topic3, data emit Transfer(alice, bob, 100); // Call that should emit the event token.transfer(bob, 100); // Record and inspect logs vm.recordLogs(); token.transfer(bob, 100); Vm.Log[] memory logs = vm.getRecordedLogs(); assertEq(logs[0].topics[0], keccak256("Transfer(address,address,uint256)")); ``` ### Testing Access Control ```solidity function test_OnlyOwner() public { // Should succeed as owner vault.adminFunction(); // Should fail as non-owner vm.expectRevert("Ownable: caller is not the owner"); vm.prank(alice); vault.adminFunction(); } ``` ## Fuzz Testing Forge automatically generates random inputs for test parameters. ### Basic Fuzz Test ```solidity function testFuzz_Deposit(uint256 amount) public { // Bound input to valid range amount = bound(amount, 0.01 ether, 100 ether); deal(alice, amount); vm.prank(alice); vault.deposit{value: amount}(); assertEq(vault.balanceOf(alice), amount); } ``` ### Fuzz Configuration ```toml # foundry.toml [fuzz] runs = 256 # Number of fuzz runs seed = 42 # Deterministic seed (optional) max_test_rejects = 65536 ``` ### Fixtures (Pre-defined Values) ```solidity // Array fixture (must match parameter name) uint256[] public fixtureAmount = [0, 1, 100, type(uint256).max]; // Function fixture function fixtureUser() public returns (address[] memory) { address[] memory users = new address[](3); users[0] = makeAddr("alice"); users[1] = makeAddr("bob"); users[2] = address(0); return users; } function testFuzz_Transfer(uint256 amount, address user) public { // amount will be one of: 0, 1, 100, max // user will be one of: alice, bob, address(0) } ``` ### Using `vm.assume()` ```solidity function testFuzz_Transfer(address recipient) public { // Skip invalid inputs vm.assume(recipient != address(0)); vm.assume(recipient != address(token)); // Or use helper assumeNotZeroAddress(recipient); assumeNotPrecompile(recipient); } ``` ## Invariant Testing Test that properties hold across random function call sequences. ### Basic Invariant Test ```solidity contract VaultInvariantTest is Test { Vault public vault; VaultHandler public handler; function setUp() public { vault = new Vault(); handler = new VaultHandler(vault); // Only fuzz the handler targetContract(address(handler)); } function invariant_SolvencyCheck() public view { assertGe( address(vault).balance, vault.totalDeposits(), "Vault is insolvent" ); } function invariant_TotalSupplyConsistent() public view { assertEq( vault.totalSupply(), sumAllBalances(), "Supply mismatch" ); } } ``` ### Handler Pattern Wrap target contract to bound inputs and track state: ```solidity contract VaultHandler is Test { Vault public vault; uint256 public ghost_totalDeposited; address[] public actors; constructor(Vault _vault) { vault = _vault; actors.push(makeAddr("alice")); actors.push(makeAddr("bob")); actors.push(makeAddr("charlie")); } function deposit(uint256 actorIndex, uint256 amount) external { // Bound inputs actorIndex = bound(actorIndex, 0, actors.length - 1); amount = bound(amount, 0.01 ether, 10 ether); address actor = actors[actorIndex]; deal(actor, amount); vm.prank(actor); vault.deposit{value: amount}(); // Track ghost variable ghost_totalDeposited += amount; } function withdraw(uint256 actorIndex, uint256 amount) external { actorIndex = bound(actorIndex, 0, actors.length - 1); address actor = actors[actorIndex]; uint256 balance = vault.balanceOf(actor); amount = bound(amount, 0, balance); vm.prank(actor); vault.withdraw(amount); ghost_totalDeposited -= amount; } } ``` ### Target Configuration ```solidity function setUp() public { // Only fuzz specific contracts targetContract(address(handler)); // Exclude contracts from fuzzing excludeContract(address(vault)); excludeContract(address(token)); // Only use specific senders targetSender(alice); targetSender(bob); // Exclude specific senders excludeSender(address(0)); // Target specific functions bytes4[] memory selectors = new bytes4[](2); selectors[0] = handler.deposit.selector; selectors[1] = handler.withdraw.selector; targetSelector(FuzzSelector({ addr: address(handler), selectors: selectors })); } ``` ### Invariant Configuration ```toml # foundry.toml [invariant] runs = 256 # Number of sequences depth = 50 # Calls per sequence fail_on_revert = false # Continue on reverts shrink_run_limit = 5000 # Shrinking iterations ``` ## Fork Testing Test against live blockchain state. ### Basic Fork Test ```solidity contract ForkTest is Test { uint256 mainnetFork; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address constant WHALE = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; function setUp() public { mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL")); vm.selectFork(mainnetFork); } function testFork_USDCTransfer() public { vm.prank(WHALE); IERC20(USDC).transfer(address(this), 1000e6); assertEq(IERC20(USDC).balanceOf(address(this)), 1000e6); } } ``` ### Fork Cheatcodes ```solidity // Create fork uint256 forkId = vm.createFork("https://eth-mainnet.alchemyapi.io/v2/..."); uint256 forkId = vm.createFork("mainnet"); // Uses foundry.toml rpc_endpoints uint256 forkId = vm.createFork("mainnet", 18000000); // Pin to block // Create and select in one call vm.createSelectFork("mainnet"); // Switch between forks vm.selectFork(mainnetFork); vm.selectFork(arbitrumFork); // Get active fork uint256 active = vm.activeFork(); // Roll fork to different block vm.rollFork(18500000); vm.rollFork(arbitrumFork, 150000000); // Make account persist across forks vm.makePersistent(address(myContract)); vm.makePersistent(alice, bob, charlie); // Check persistence bool isPersistent = vm.isPersistent(address(myContract)); // Revoke persistence vm.revokePersistent(address(myContract)); ``` ### Multi-Fork Testing ```solidity function testFork_CrossChain() public { // Deploy on mainnet vm.selectFork(mainnetFork); address mainnetToken = address(new Token()); // Deploy on Arbitrum vm.selectFork(arbitrumFork); address arbitrumToken = address(new Token()); // Verify different deployments vm.selectFork(mainnetFork); assertEq(Token(mainnetToken).name(), "Token"); vm.selectFork(arbitrumFork); assertEq(Token(arbitrumToken).name(), "Token"); } ``` ### Fork Configuration ```toml # foundry.toml [rpc_endpoints] mainnet = "${MAINNET_RPC_URL}" sepolia = "${SEPOLIA_RPC_URL}" arbitrum = "${ARBITRUM_RPC_URL}" optimism = "${OPTIMISM_RPC_URL}" ``` ## Differential Testing Compare implementations against reference. ```solidity function testDifferential_MerkleRoot(bytes32[] memory leaves) public { vm.assume(leaves.length > 0 && leaves.length < 100); // Solidity implementation bytes32 solidityRoot = merkle.getRoot(leaves); // Reference implementation (via FFI) string[] memory cmd = new string[](3); cmd[0] = "node"; cmd[1] = "scripts/merkle.js"; cmd[2] = vm.toString(abi.encode(leaves)); bytes memory result = vm.ffi(cmd); bytes32 jsRoot = abi.decode(result, (bytes32)); assertEq(solidityRoot, jsRoot, "Merkle root mismatch"); } ``` ## Complete Cheatcode Reference ### State Manipulation ```solidity // ETH balance vm.deal(address, uint256); // Code at address vm.etch(address, bytes memory code); // Storage vm.store(address, bytes32 slot, bytes32 value); bytes32 value = vm.load(address, bytes32 slot); // Nonce vm.setNonce(address, uint64 nonce); vm.resetNonce(address); uint64 nonce = vm.getNonce(address); // Copy storage vm.copyStorage(address from, address to); ``` ### Caller Context ```solidity // Single call vm.prank(address sender); vm.prank(address sender, address origin); // Multiple calls vm.startPrank(address sender); vm.startPrank(address sender, address origin); vm.stopPrank(); ``` ### Time & Block ```solidity vm.warp(uint256 timestamp); // block.timestamp vm.roll(uint256 blockNumber); // block.number vm.fee(uint256 gasPrice); // tx.gasprice vm.difficulty(uint256 difficulty); // block.difficulty vm.coinbase(address miner); // block.coinbase vm.chainId(uint256 chainId); // block.chainid vm.prevrandao(bytes32 prevrandao); // block.prevrandao ``` ### Expectations ```solidity // Reverts vm.expectRevert(); vm.expectRevert(bytes memory message); vm.expectRevert(bytes4 selector); vm.expectPartialRevert(bytes4 selector); // Events vm.expectEmit(bool topic1, bool topic2, bool topic3, bool data); vm.expectEmit(bool topic1, bool topic2, bool topic3, bool data, address emitter); // Calls vm.expectCall(address target, bytes memory data); vm.expectCall(address target, uint256 value, bytes memory data); vm.expectCall(address target, bytes memory data, uint64 count); ``` ### Snapshots ```solidity uint256 snapshot = vm.snapshot(); vm.revertTo(snapshot); vm.revertToAndDelete(snapshot); ``` ### Environment ```solidity // Read .env string memory value = vm.envString("KEY"); uint256 value = vm.envUint("KEY"); address value = vm.envAddress("KEY"); bool value = vm.envBool("KEY"); bytes32 value = vm.envBytes32("KEY"); // With default uint256 value = vm.envOr("KEY", uint256(100)); // Check existence bool exists = vm.envExists("KEY"); ``` ### Cryptography ```solidity // Sign message (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); (bytes32 r, bytes32 vs) = vm.signCompact(privateKey, digest); // Get address from private key address addr = vm.addr(privateKey); // Derive key from mnemonic uint256 key = vm.deriveKey(mnemonic, index); uint256 key = vm.deriveKey(mnemonic, path, index); // Create wallet Vm.Wallet memory wallet = vm.createWallet("label"); Vm.Wallet memory wallet = vm.createWallet(privateKey); ``` ### Labels & Debugging ```solidity vm.label(address, "name"); string memory name = vm.getLabel(address); vm.breakpoint(); // Debugger breakpoint vm.breakpoint("name"); // Named breakpoint ``` ### Gas Metering ```solidity vm.pauseGasMetering(); // ... expensive operations ... vm.resumeGasMetering(); Vm.Gas memory gas = vm.lastCallGas(); ``` ### File I/O ```solidity string memory content = vm.readFile("path/to/file"); vm.writeFile("path/to/file", "content"); bool exists = vm.exists("path/to/file"); vm.removeFile("path/to/file"); ``` ## Test Verbosity Levels ```bash forge test # Summary only forge test -v # + Logs forge test -vv # + Assertion errors forge test -vvv # + Stack traces (failures) forge test -vvvv # + Stack traces (all) + setup forge test -vvvvv # + All traces always ``` ## Best Practices ### Test Organization 1. One test file per contract 2. Group related tests in same contract 3. Use descriptive function names 4. Include failure reason in test name ### Fuzz Testing 1. Always `bound()` numeric inputs 2. Use `vm.assume()` sparingly (reduces coverage) 3. Use fixtures for edge cases 4. Set deterministic seed in CI ### Invariant Testing 1. Use handler pattern for complex protocols 2. Track ghost variables for assertions 3. Start with simple invariants 4. Increase depth/runs gradually ### Fork Testing 1. Pin to specific block for reproducibility 2. Configure RPC endpoints in foundry.toml 3. Use `vm.makePersistent()` for deployed contracts 4. Cache responses with deterministic seed ### General 1. Test public interface, not internal state 2. Use `makeAddr()` for labeled addresses 3. Use `deal()` instead of minting 4. Label important addresses for traces 5. Test both success and failure paths