跳到主要内容

5、Foundry 测试最佳实践

测试组织和结构

项目结构最佳实践

test/
├── unit/ # 单元测试
│ ├── Token.t.sol
│ └── Vault.t.sol
├── integration/ # 集成测试
│ ├── VaultIntegration.t.sol
│ └── TokenVaultFlow.t.sol
├── fuzz/ # 模糊测试
│ ├── VaultFuzz.t.sol
│ └── TokenFuzz.t.sol
├── invariant/ # 不变量测试
│ ├── VaultInvariant.t.sol
│ └── handlers/
│ └── VaultHandler.sol
├── fork/ # 分叉测试
│ └── MainnetFork.t.sol
├── utils/ # 测试工具
│ ├── TestHelper.sol
│ └── MockContracts.sol
└── Base.t.sol # 基础测试合约

基础测试合约

// Base.t.sol - 所有测试的基础合约
abstract contract BaseTest is Test {
// 常用地址
address internal constant ALICE = address(0x1);
address internal constant BOB = address(0x2);
address internal constant CHARLIE = address(0x3);
address internal constant ATTACKER = address(0x4);

// 常用常量
uint256 internal constant INITIAL_BALANCE = 10000 ether;
uint256 internal constant PRECISION = 1e18;
uint256 internal constant TOLERANCE = 1e15; // 0.1%

// 事件定义
event Transfer(address indexed from, address indexed to, uint256 value);
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);

// 修饰符
modifier pranking(address user) {
vm.startPrank(user);
_;
vm.stopPrank();
}

// 通用辅助函数
function _dealAndApprove(
IERC20 token,
address user,
uint256 amount,
address spender
) internal {
deal(address(token), user, amount);
vm.prank(user);
token.approve(spender, amount);
}

function _expectRevertWithMessage(string memory message) internal {
vm.expectRevert(bytes(message));
}
}

命名约定

测试函数命名

contract VaultTest is BaseTest {
// ✅ 好的命名
function test_DepositIncreasesUserBalance() public { }
function test_WithdrawDecreasesTotalSupply() public { }
function test_RevertWhen_DepositAmountIsZero() public { }
function test_RevertWhen_InsufficientBalance() public { }

// 模糊测试
function testFuzz_DepositWithdrawRoundTrip(uint256 amount) public { }

// 不变量测试
function invariant_TotalSupplyEqualsUserShares() public { }

// 分叉测试
function testFork_InteractWithUniswap() public { }

// ❌ 不好的命名
function test1() public { }
function testDeposit() public { } // 太模糊
function test_deposit_works() public { } // 不描述具体行为
}

变量命名

function test_DepositIncreasesUserBalance() public {
// ✅ 描述性变量名
uint256 depositAmount = 100 ether;
uint256 userBalanceBefore = token.balanceOf(ALICE);
uint256 vaultBalanceBefore = token.balanceOf(address(vault));

vm.prank(ALICE);
vault.deposit(depositAmount);

uint256 userBalanceAfter = token.balanceOf(ALICE);
uint256 vaultBalanceAfter = token.balanceOf(address(vault));

assertEq(userBalanceAfter, userBalanceBefore - depositAmount);
assertEq(vaultBalanceAfter, vaultBalanceBefore + depositAmount);
}

测试数据管理

使用常量和配置

contract VaultTestConfig {
// 测试配置
struct TestConfig {
uint256 initialBalance;
uint256 depositAmount;
uint256 withdrawAmount;
address[] users;
}

function getDefaultConfig() internal pure returns (TestConfig memory) {
address[] memory users = new address[](4);
users[0] = address(0x1);
users[1] = address(0x2);
users[2] = address(0x3);
users[3] = address(0x4);

return TestConfig({
initialBalance: 10000 ether,
depositAmount: 1000 ether,
withdrawAmount: 500 ether,
users: users
});
}

function getStressTestConfig() internal pure returns (TestConfig memory) {
// 压力测试配置
TestConfig memory config = getDefaultConfig();
config.initialBalance = 1000000 ether;
config.depositAmount = 100000 ether;
return config;
}
}

测试数据工厂

library TestDataFactory {
function createUsers(uint256 count) internal pure returns (address[] memory) {
address[] memory users = new address[](count);
for (uint256 i = 0; i < count; i++) {
users[i] = address(uint160(i + 1));
}
return users;
}

function createTokenAmounts(
uint256 count,
uint256 baseAmount
) internal pure returns (uint256[] memory) {
uint256[] memory amounts = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
amounts[i] = baseAmount * (i + 1);
}
return amounts;
}
}

断言最佳实践

使用适当的断言

function test_AssertionBestPractices() public {
uint256 amount = 100 ether;

// ✅ 使用具体的断言函数
assertEq(vault.balanceOf(ALICE), amount, "User balance should equal deposit");
assertGt(vault.totalSupply(), 0, "Total supply should be greater than 0");
assertLe(amount, INITIAL_BALANCE, "Amount should not exceed initial balance");

// ✅ 使用近似相等处理精度问题
uint256 expectedValue = 999999999999999999; // 接近 1 ether
uint256 actualValue = 1 ether;
assertApproxEqAbs(actualValue, expectedValue, 1, "Values should be approximately equal");

// ✅ 使用相对误差
assertApproxEqRel(actualValue, expectedValue, 0.001e18, "Values should be within 0.1%");

// ❌ 避免使用 assertTrue 进行数值比较
// assertTrue(vault.balanceOf(ALICE) == amount); // 不好
}

错误消息最佳实践

function test_ErrorMessageBestPractices() public {
uint256 amount = 100 ether;

// ✅ 提供有意义的错误消息
assertEq(
vault.balanceOf(ALICE),
amount,
"User should receive shares equal to deposit amount"
);

// ✅ 包含实际值和期望值
assertEq(
vault.balanceOf(ALICE),
amount,
string.concat(
"Expected balance: ", vm.toString(amount),
", Actual balance: ", vm.toString(vault.balanceOf(ALICE))
)
);

// ✅ 使用上下文信息
assertGt(
token.balanceOf(address(vault)),
0,
"Vault should hold tokens after deposit"
);
}

模糊测试最佳实践

输入验证和边界

function testFuzz_DepositWithValidation(uint256 amount) public {
// ✅ 使用 bound 限制输入范围
amount = bound(amount, 1, INITIAL_BALANCE);

// ✅ 或使用 vm.assume 过滤输入
// vm.assume(amount > 0 && amount <= INITIAL_BALANCE);

_dealAndApprove(token, ALICE, amount, address(vault));

vm.prank(ALICE);
vault.deposit(amount);

assertEq(vault.balanceOf(ALICE), amount);
}

function testFuzz_TransferWithAssumptions(
address from,
address to,
uint256 amount
) public {
// ✅ 使用 assume 过滤无效输入
vm.assume(from != address(0));
vm.assume(to != address(0));
vm.assume(from != to);
vm.assume(amount > 0);
vm.assume(amount <= type(uint128).max); // 避免溢出

_dealAndApprove(token, from, amount, to);

vm.prank(from);
bool success = token.transfer(to, amount);

assertTrue(success);
assertEq(token.balanceOf(to), amount);
}

属性测试

function testFuzz_DepositWithdrawProperty(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, INITIAL_BALANCE);

_dealAndApprove(token, ALICE, depositAmount, address(vault));

uint256 balanceBefore = token.balanceOf(ALICE);

// 存款
vm.prank(ALICE);
vault.deposit(depositAmount);

// 立即全部取出
uint256 shares = vault.balanceOf(ALICE);
vm.prank(ALICE);
vault.withdraw(shares);

uint256 balanceAfter = token.balanceOf(ALICE);

// 属性:存取后余额应该相等(忽略精度损失)
assertApproxEqAbs(
balanceAfter,
balanceBefore,
1,
"Balance should be preserved in deposit-withdraw cycle"
);
}

不变量测试最佳实践

有意义的不变量

contract VaultInvariantTest is BaseTest {
VaultHandler public handler;

function setUp() public {
token = new Token();
vault = new Vault(address(token));
handler = new VaultHandler(vault, token);

targetContract(address(handler));
}

// ✅ 核心业务不变量
function invariant_TotalSupplyEqualsUserShares() public {
assertEq(
vault.totalSupply(),
handler.getTotalUserShares(),
"Total supply must equal sum of user shares"
);
}

// ✅ 资产保护不变量
function invariant_VaultHoldsEnoughTokens() public {
uint256 vaultBalance = token.balanceOf(address(vault));
uint256 totalSupply = vault.totalSupply();

assertGe(
vaultBalance,
totalSupply,
"Vault must hold at least as many tokens as total shares"
);
}

// ✅ 数学不变量
function invariant_SharesNeverExceedTokens() public {
assertLe(
vault.totalSupply(),
token.balanceOf(address(vault)),
"Shares should never exceed token balance"
);
}
}

处理器模式

contract VaultHandler is BaseTest {
Vault public vault;
Token public token;

address[] public users;
uint256 public totalUserShares;

constructor(Vault _vault, Token _token) {
vault = _vault;
token = _token;

// 创建测试用户
users.push(ALICE);
users.push(BOB);
users.push(CHARLIE);
}

function deposit(uint256 amount, uint256 userIndex) public {
// 限制参数范围
amount = bound(amount, 1, 1000 ether);
userIndex = bound(userIndex, 0, users.length - 1);

address user = users[userIndex];

// 准备代币
deal(address(token), user, amount);

vm.prank(user);
token.approve(address(vault), amount);

// 执行存款
vm.prank(user);
vault.deposit(amount);

// 更新跟踪状态
totalUserShares += amount;
}

function withdraw(uint256 sharePercent, uint256 userIndex) public {
userIndex = bound(userIndex, 0, users.length - 1);
sharePercent = bound(sharePercent, 1, 100);

address user = users[userIndex];
uint256 userShares = vault.balanceOf(user);

if (userShares == 0) return;

uint256 sharesToWithdraw = (userShares * sharePercent) / 100;

vm.prank(user);
vault.withdraw(sharesToWithdraw);

// 更新跟踪状态
totalUserShares -= sharesToWithdraw;
}

function getTotalUserShares() public view returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < users.length; i++) {
total += vault.balanceOf(users[i]);
}
return total;
}
}

测试隔离和清理

正确的 setUp 和 tearDown

contract VaultTest is BaseTest {
Token public token;
Vault public vault;

function setUp() public {
// ✅ 每个测试都有干净的状态
token = new Token();
vault = new Vault(address(token));

// 设置初始状态
_setupUsers();
}

function _setupUsers() internal {
address[] memory users = new address[](3);
users[0] = ALICE;
users[1] = BOB;
users[2] = CHARLIE;

for (uint256 i = 0; i < users.length; i++) {
deal(address(token), users[i], INITIAL_BALANCE);
vm.prank(users[i]);
token.approve(address(vault), type(uint256).max);
}
}

// ✅ 测试之间相互独立
function test_FirstTest() public {
vm.prank(ALICE);
vault.deposit(100 ether);
assertEq(vault.balanceOf(ALICE), 100 ether);
}

function test_SecondTest() public {
// 这个测试不受第一个测试影响
assertEq(vault.balanceOf(ALICE), 0);

vm.prank(BOB);
vault.deposit(200 ether);
assertEq(vault.balanceOf(BOB), 200 ether);
}
}

错误处理测试

全面的错误测试

contract ErrorHandlingTest is BaseTest {
function test_RevertConditions() public {
// ✅ 测试所有 revert 条件

// 零值存款
vm.prank(ALICE);
vm.expectRevert("Amount must be greater than 0");
vault.deposit(0);

// 余额不足
vm.prank(ALICE);
vm.expectRevert("ERC20: insufficient balance");
vault.deposit(INITIAL_BALANCE + 1);

// 未授权提取
vm.prank(ALICE);
vm.expectRevert("ERC20: insufficient balance");
vault.withdraw(1);

// 提取超过拥有的份额
vm.prank(ALICE);
vault.deposit(100 ether);

vm.prank(ALICE);
vm.expectRevert();
vault.withdraw(101 ether);
}

function test_CustomErrors() public {
// ✅ 测试自定义错误
vm.expectRevert(
abi.encodeWithSelector(
InsufficientBalance.selector,
0,
100 ether
)
);
vault.withdrawWithCustomError(100 ether);
}
}

性能和 Gas 测试

Gas 使用监控

contract GasTest is BaseTest {
function test_DepositGasUsage() public {
uint256 gasBefore = gasleft();

vm.prank(ALICE);
vault.deposit(100 ether);

uint256 gasUsed = gasBefore - gasleft();

// ✅ 设置合理的 gas 限制
assertLt(gasUsed, 100000, "Deposit should use less than 100k gas");

// ✅ 记录 gas 使用情况
console.log("Deposit gas used:", gasUsed);
}

function test_BatchOperationGasEfficiency() public {
uint256 singleOpGas = _measureSingleDeposit();
uint256 batchOpGas = _measureBatchDeposit(10);

// ✅ 验证批量操作的效率
uint256 expectedBatchGas = singleOpGas * 10;
uint256 gasEfficiency = ((expectedBatchGas - batchOpGas) * 100) / expectedBatchGas;

console.log("Gas efficiency:", gasEfficiency, "%");
assertGt(gasEfficiency, 20, "Batch operation should be at least 20% more efficient");
}

function _measureSingleDeposit() internal returns (uint256) {
uint256 gasBefore = gasleft();
vm.prank(ALICE);
vault.deposit(10 ether);
return gasBefore - gasleft();
}

function _measureBatchDeposit(uint256 count) internal returns (uint256) {
uint256[] memory amounts = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
amounts[i] = 10 ether;
}

uint256 gasBefore = gasleft();
vm.prank(ALICE);
vault.batchDeposit(amounts);
return gasBefore - gasleft();
}
}

文档和注释

测试文档

/**
* @title VaultTest
* @notice 全面的 Vault 合约测试套件
* @dev 包含单元测试、集成测试、边界测试和攻击场景测试
*
* 测试覆盖范围:
* - 基础存取款功能
* - 多用户交互
* - 边界条件和错误处理
* - 通胀攻击防护
* - Gas 使用优化
*/
contract VaultTest is BaseTest {
/**
* @notice 测试基础存款功能
* @dev 验证:
* 1. 用户代币余额正确减少
* 2. vault 代币余额正确增加
* 3. 用户获得正确数量的份额
* 4. 总供应量正确更新
*/
function test_BasicDeposit() public {
// 实现...
}

/**
* @notice 测试通胀攻击场景
* @dev 攻击步骤:
* 1. 攻击者存入最小金额(1 wei)
* 2. 攻击者直接转账大量代币给 vault
* 3. 受害者存款时由于精度损失获得 0 份额
* 4. 攻击者提取时获得大部分资金
*
* 预期结果:攻击者能够获利,受害者损失资金
*/
function test_InflationAttack() public {
// 实现...
}
}

持续改进

测试度量和监控

contract TestMetrics is BaseTest {
uint256 public totalTests;
uint256 public passedTests;
uint256 public totalGasUsed;

modifier trackTest() {
totalTests++;
uint256 gasBefore = gasleft();
_;
totalGasUsed += gasBefore - gasleft();
passedTests++;
}

function test_WithMetrics() public trackTest {
// 测试实现
vault.deposit(100 ether);
assertEq(vault.totalSupply(), 100 ether);
}

function printTestSummary() public view {
console.log("=== Test Summary ===");
console.log("Total tests:", totalTests);
console.log("Passed tests:", passedTests);
console.log("Success rate:", (passedTests * 100) / totalTests, "%");
console.log("Average gas per test:", totalGasUsed / totalTests);
}
}

下一步

掌握最佳实践后,继续学习:

📢 Share this article