Testing with Foundry
Any test function with parameters is a property-based fuzz test: Foundry generates hundreds of random inputs (biased toward edge cases).
34.1 Unit tests
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {SafeBank} from "../src/SafeBank.sol";
contract SafeBankTest is Test {
SafeBank bank;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
event Deposited(address indexed user, uint256 amount);
function setUp() public {
bank = new SafeBank();
vm.deal(alice, 10 ether); // set ETH balance
vm.deal(bob, 10 ether);
}
function test_Deposit() public {
vm.prank(alice); // next call's msg.sender = alice
bank.deposit{value: 1 ether}();
assertEq(bank.balances(alice), 1 ether);
assertEq(address(bank).balance, 1 ether);
}
function test_Withdraw() public {
vm.startPrank(alice); // all calls until stopPrank
bank.deposit{value: 2 ether}();
uint256 before = alice.balance;
bank.withdraw();
vm.stopPrank();
assertEq(alice.balance, before + 2 ether);
assertEq(bank.balances(alice), 0);
}
function test_RevertWhen_NoBalance() public {
vm.expectRevert(bytes("no b
…forge test # all tests
forge test --match-test test_Withdraw -vvvv # filter + full traces
forge test --match-contract SafeBankTest
forge coverage # line/branch coverage
forge test --gas-report
forge snapshot # write .gas-snapshot; `forge snapshot --diff` to compare34.2 Common cheatcodes
vm.prank(addr) / vm.startPrank / vm.stopPrank spoof msg.sender vm.deal(addr, amount) set ETH balance deal(token, addr, amount) (StdCheats) set ERC-20 balance vm.warp(ts) / vm.roll(n) set block.timestamp / block.number next call must revert (optionally with specific data)
vm.expectEmit(true, true, false, true) then emit Event(...) assert the next emitted event vm.expectCall(target, data) assert a call happens vm.mockCall(target, data, ret) mock a call's return vm.store / vm.load write/read raw storage vm.sign(pk, digest) / vm.addr(pk) sign and derive addresses in tests makeAddr("name") / makeAddrAndKey labeled test addresses vm.createSelectFork(rpcUrl, block) fork a live chain
vm.assume(cond) / bound(x, min, max) constrain fuzz inputs
vm.snapshotState() / vm.revertToState(id) EVM state snapshots
34.3 Fuzz testing
Any test function with parameters is a property-based fuzz test: Foundry generates hundreds of random inputs (biased toward edge cases).
function testFuzz_DepositWithdraw(uint96 amount) public {
vm.assume(amount > 0);
vm.deal(alice, amount);
vm.startPrank(alice);
bank.deposit{value: amount}();
bank.withdraw();
vm.stopPrank();
assertEq(alice.balance, amount);
}
function testFuzz_FeeNeverExceedsAmount(uint256 amount, uint16 feeBps) public pure {
amount = bound(amount, 0, 1e30);
feeBps = uint16(bound(feeBps, 0, 10_000));
uint256 fee = amount * feeBps / 10_000;
assertLe(fee, amount);
}34.4 Invariant (stateful) testing
Invariant tests call random sequences of functions on target contracts and check properties that must always hold.
Configure depth and runs in foundry.toml under [invariant] (runs, depth, fail_on_revert).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {SafeBank} from "../src/SafeBank.sol";
contract BankHandler is Test {
SafeBank public bank;
uint256 public ghostDeposited; // "ghost variables" track expected totals
uint256 public ghostWithdrawn;
address[] public actors;
constructor(SafeBank _bank) {
bank = _bank;
actors.push(makeAddr("a1"));
actors.push(makeAddr("a2"));
actors.push(makeAddr("a3"));
}
function deposit(uint256 actorSeed, uint256 amount) external {
address actor = actors[actorSeed % actors.length];
amount = bound(amount, 1, 100 ether);
vm.deal(actor, amount);
vm.prank(actor);
bank.deposit{value: amount}();
ghostDeposited += amount;
}
function withdraw(uint256 actorSeed) external {
address actor = actors[actorSeed % actors.length];
uint256 bal = bank.balances(actor);
if (bal == 0) return;
vm.prank(actor);
bank.withdraw();
ghostWithdrawn += bal;
}
}
contract BankInvariantTest is Test {
SafeBank bank;
BankHandler handler;
f
…34.5 Fork testing
Fork tests let you test integrations against real protocols (Uniswap, Aave, Chainlink) and reproduce exploits.
function setUp() public {
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 21_000_000); // pin a block for reproducibility
}
function test_UsdcBalance() public view {
IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
assertGt(usdc.totalSupply(), 0);
}34.6 Other fuzzers
Echidna and Medusa (Trail of Bits / Crytic) are dedicated property-based fuzzers with powerful corpus and coverage guidance; Chimera lets you write one set of properties for Foundry, Echidna, and Medusa.