2、Writing Tests
Learn how to write effective tests for your smart contracts using Foundry.
Test Contract Structure
A typical test contract follows this structure:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {MyContract} from "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract public myContract;
address public user1;
address public user2;
function setUp() public {
// Setup runs before each test
myContract = new MyContract();
user1 = address(0x1);
user2 = address(0x2);
}
function test_BasicFunctionality() public {
// Test implementation
}
}
Assertion Functions
Foundry provides various assertion functions through forge-std/Test.sol:
Equality Assertions
// Assert equality
assertEq(a, b);
assertEq(a, b, "Custom error message");
// Assert inequality
assertNotEq(a, b);
// For approximate equality (useful for decimals)
assertApproxEqAbs(a, b, maxDelta);
assertApproxEqRel(a, b, maxPercentDelta);
Boolean Assertions
// Assert true/false
assertTrue(condition);
assertFalse(condition);
Greater Than / Less Than
assertGt(a, b); // a > b
assertGe(a, b); // a >= b
assertLt(a, b); // a < b
assertLe(a, b); // a <= b
Test Types and Patterns
Unit Tests
Test individual functions in isolation:
function test_Transfer() public {
token.mint(user1, 100);
vm.prank(user1);
token.transfer(user2, 50);
assertEq(token.balanceOf(user1), 50);
assertEq(token.balanceOf(user2), 50);
}
Integration Tests
Test multiple contracts working together:
function test_SwapOnUniswap() public {
// Setup multiple contracts
token1.approve(address(router), 100);
router.swapExactTokensForTokens(...);
// Assert final state
}
Fuzz Tests
Test with random inputs:
function testFuzz_Transfer(uint256 amount) public {
vm.assume(amount <= token.totalSupply());
token.mint(user1, amount);
vm.prank(user1);
token.transfer(user2, amount);
assertEq(token.balanceOf(user2), amount);
}
Error Handling and Reverts
Testing Expected Reverts
function test_RevertWhen_InsufficientBalance() public {
vm.expectRevert("Insufficient balance");
token.transfer(user2, 1000);
}
// With custom errors
function test_RevertWhen_Unauthorized() public {
vm.expectRevert(Unauthorized.selector);
myContract.adminFunction();
}
Testing Require Statements
function test_RequireValidInput() public {
vm.expectRevert("Invalid input");
myContract.setNumber(0);
}
Event Testing
Basic Event Testing
function test_EmitsTransferEvent() public {
// Tell Foundry which event to expect
vm.expectEmit(true, true, false, true);
// Emit the expected event
emit Transfer(user1, user2, 100);
// Call the function that should emit the event
token.transfer(user2, 100);
}
Multiple Events
function test_EmitsMultipleEvents() public {
vm.expectEmit(true, true, false, true);
emit Approval(user1, address(router), 100);
vm.expectEmit(true, true, false, true);
emit Transfer(user1, user2, 50);
// Function that emits both events
token.approveAndTransfer(address(router), 100, user2, 50);
}
Cheatcodes for Testing
Manipulating State
// Change msg.sender for next call
vm.prank(user1);
myContract.doSomething();
// Change msg.sender for all subsequent calls
vm.startPrank(user1);
myContract.doSomething();
myContract.doSomethingElse();
vm.stopPrank();
// Deal ETH to address
vm.deal(user1, 10 ether);
// Set contract storage
vm.store(address(token), slot, value);
Time Manipulation
// Set block timestamp
vm.warp(1641070800);
// Increase time
vm.warp(block.timestamp + 1 days);
// Set block number
vm.roll(12345678);
Mocking Calls
// Mock a call to a contract
vm.mockCall(
address(oracle),
abi.encodeWithSelector(Oracle.getPrice.selector),
abi.encode(1000)
);
Test Organization
Using setUp for Common Setup
function setUp() public {
// Common setup for all tests
token = new Token();
user1 = makeAddr("user1");
user2 = makeAddr("user2");
vm.deal(user1, 100 ether);
vm.deal(user2, 100 ether);
}
Helper Functions
function _mintAndApprove(address user, uint256 amount) internal {
token.mint(user, amount);
vm.prank(user);
token.approve(address(this), amount);
}
function test_WithHelper() public {
_mintAndApprove(user1, 100);
// Continue test
}
Test Modifiers
modifier funded(address user) {
vm.deal(user, 100 ether);
_;
}
function test_WithModifier() public funded(user1) {
// user1 now has 100 ETH
}
Advanced Patterns
Testing Access Control
function test_OnlyOwnerCanCall() public {
vm.prank(owner);
myContract.adminFunction(); // Should succeed
vm.prank(user1);
vm.expectRevert("Not owner");
myContract.adminFunction(); // Should fail
}
Testing State Transitions
function test_StateTransition() public {
assertEq(uint(myContract.state()), uint(State.Pending));
myContract.start();
assertEq(uint(myContract.state()), uint(State.Active));
myContract.complete();
assertEq(uint(myContract.state()), uint(State.Completed));
}
Snapshot and Revert
function test_WithSnapshot() public {
uint256 snapshot = vm.snapshot();
// Make some state changes
myContract.doSomething();
assertEq(myContract.value(), 100);
// Revert to snapshot
vm.revertTo(snapshot);
assertEq(myContract.value(), 0);
}
Best Practices
- Test One Thing: Each test should verify one specific behavior
- Clear Names: Use descriptive test names like
test_RevertWhen_InsufficientBalance - Arrange-Act-Assert: Structure tests clearly
- Use setUp: Put common setup in
setUp()function - Test Edge Cases: Don't just test happy paths
- Add Comments: Explain complex test logic
Next Steps
- Advanced Testing - Learn about fuzzing and invariant testing
- Debugging Tools - Master debugging techniques
- Best Practices - Learn testing best practices