3、Advanced Testing
Master advanced testing techniques including fuzzing, invariant testing, and property-based testing.
Fuzz Testing
Fuzz testing automatically generates random inputs to test your contracts.
Basic Fuzzing
function testFuzz_Addition(uint256 a, uint256 b) public {
vm.assume(a < type(uint256).max - b); // Prevent overflow
uint256 result = calculator.add(a, b);
assertEq(result, a + b);
}
Multiple Parameters
function testFuzz_Transfer(address from, address to, uint256 amount) public {
vm.assume(from != address(0));
vm.assume(to != address(0));
vm.assume(from != to);
vm.assume(amount <= token.totalSupply());
token.mint(from, amount);
vm.prank(from);
token.transfer(to, amount);
assertEq(token.balanceOf(to), amount);
}
Configuring Fuzz Runs
# foundry.toml
[fuzz]
runs = 1000
max_test_rejects = 65536
Invariant Testing
Invariant tests ensure certain properties always hold true, no matter what actions are taken.
Basic Invariant Test
contract TokenInvariantTest is Test {
Token public token;
Handler public handler;
function setUp() public {
token = new Token();
handler = new Handler(token);
targetContract(address(handler));
}
function invariant_TotalSupplyMatchesBalances() public {
uint256 sumOfBalances = handler.sumOfBalances();
assertEq(token.totalSupply(), sumOfBalances);
}
}
Handler Pattern
contract Handler is Test {
Token public token;
uint256 public sumOfBalances;
constructor(Token _token) {
token = _token;
}
function mint(address to, uint256 amount) public {
amount = bound(amount, 0, 1e18);
to = address(uint160(bound(uint160(to), 1, type(uint160).max)));
token.mint(to, amount);
sumOfBalances += amount;
}
function burn(address from, uint256 amount) public {
amount = bound(amount, 0, token.balanceOf(from));
vm.prank(from);
token.burn(amount);
sumOfBalances -= amount;
}
}
Property-Based Testing
Test properties that should always be true:
function testProperty_Commutative(uint256 a, uint256 b) public {
vm.assume(a < type(uint128).max);
vm.assume(b < type(uint128).max);
assertEq(calculator.add(a, b), calculator.add(b, a));
}
function testProperty_Associative(uint256 a, uint256 b, uint256 c) public {
vm.assume(a < type(uint85).max);
vm.assume(b < type(uint85).max);
vm.assume(c < type(uint85).max);
uint256 left = calculator.add(calculator.add(a, b), c);
uint256 right = calculator.add(a, calculator.add(b, c));
assertEq(left, right);
}
State Machine Testing
Test complex state transitions:
contract StateMachineTest is Test {
MyContract public myContract;
enum Action { Create, Start, Pause, Resume, Complete }
function testFuzz_ValidStateTransitions(Action[] memory actions) public {
vm.assume(actions.length > 0 && actions.length <= 10);
for (uint i = 0; i < actions.length; i++) {
try this.executeAction(actions[i]) {
// Action succeeded
} catch {
// Action failed - ensure it was an invalid transition
assertTrue(isInvalidTransition(myContract.state(), actions[i]));
}
}
}
function executeAction(Action action) public {
if (action == Action.Create) myContract.create();
else if (action == Action.Start) myContract.start();
else if (action == Action.Pause) myContract.pause();
else if (action == Action.Resume) myContract.resume();
else if (action == Action.Complete) myContract.complete();
}
}
Differential Testing
Compare implementations:
function testDifferential_AgainstReference(uint256 a, uint256 b) public {
vm.assume(b != 0);
uint256 optimizedResult = optimizedDiv.divide(a, b);
uint256 referenceResult = referenceDiv.divide(a, b);
assertEq(optimizedResult, referenceResult);
}
Fork Testing
Test against real blockchain state:
contract ForkTest is Test {
uint256 mainnetFork;
function setUp() public {
mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"));
vm.selectFork(mainnetFork);
}
function test_SwapOnUniswap() public {
address UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
// Test against real Uniswap contracts
deal(WETH, address(this), 1 ether);
// ... perform swap and assertions
}
}
Advanced Fuzzing Techniques
Bounded Fuzzing
function testFuzz_BoundedInput(uint256 amount) public {
amount = bound(amount, 1, 1000000);
// Test with bounded amount
token.mint(user, amount);
assertLe(amount, 1000000);
assertGe(amount, 1);
}
Structured Fuzzing
struct FuzzInput {
address user;
uint256 amount;
uint256 deadline;
}
function testFuzz_Structured(FuzzInput memory input) public {
input.user = address(uint160(bound(uint160(input.user), 1, type(uint160).max)));
input.amount = bound(input.amount, 1, 1e18);
input.deadline = bound(input.deadline, block.timestamp, block.timestamp + 365 days);
// Use structured input
}
Assume vs Bound
Using vm.assume
function testFuzz_WithAssume(uint256 amount) public {
vm.assume(amount > 0);
vm.assume(amount <= 1e18);
// Test with valid amount
}
Using bound
function testFuzz_WithBound(uint256 amount) public {
amount = bound(amount, 1, 1e18);
// Test with bounded amount
}
Tip: Prefer bound over vm.assume for better performance.
Invariant Testing Best Practices
1. Use Handlers
Always use handlers to constrain the action space:
contract Handler is Test {
MyContract public target;
function validAction(uint256 input) public {
input = bound(input, MIN, MAX);
target.action(input);
}
}
2. Track Ghost Variables
contract Handler is Test {
uint256 public ghost_totalDeposited;
uint256 public ghost_totalWithdrawn;
function deposit(uint256 amount) public {
vault.deposit(amount);
ghost_totalDeposited += amount;
}
function withdraw(uint256 amount) public {
vault.withdraw(amount);
ghost_totalWithdrawn += amount;
}
}
3. Multiple Invariants
function invariant_SolvencyMaintained() public {
assertGe(vault.totalAssets(), vault.totalLiabilities());
}
function invariant_AccountingConsistent() public {
assertEq(
handler.ghost_totalDeposited() - handler.ghost_totalWithdrawn(),
vault.totalAssets()
);
}
Next Steps
- Debugging Tools - Learn debugging techniques
- Best Practices - Master testing best practices
- Real World Examples - See advanced testing in action