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 compare

34.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.