跳到主要内容

1. 为什么测试是合约开发的主线任务

智能合约一旦上线就很难回滚,测试不是“收尾动作”,而是开发主流程。建议把测试分成四层:

层级目标Foundry 工具
单元测试(Unit)验证单个函数逻辑forge test --match-test
模糊测试(Fuzz)验证输入边界和随机输入安全性forge test(自动 fuzz)
不变量测试(Invariant)验证系统始终满足关键性质forge test --match-contract
分叉测试(Fork)在真实链状态下验证集成行为anvil --fork-url / vm.createSelectFork

2. 单元测试与 Fuzz 测试示例

先写一个最小金库合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

error ZeroAmount();
error InsufficientBalance(uint256 requested, uint256 available);

contract Vault {
mapping(address => uint256) public balanceOf;

function deposit() external payable {
if (msg.value == 0) revert ZeroAmount();
balanceOf[msg.sender] += msg.value;
}

function withdraw(uint256 amount) external {
uint256 bal = balanceOf[msg.sender];
if (amount > bal) revert InsufficientBalance(amount, bal);

balanceOf[msg.sender] = bal - amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "ETH_TRANSFER_FAILED");
}
}

对应测试:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";

contract VaultTest is Test {
Vault internal vault;
address internal alice = makeAddr("alice");

function setUp() public {
vault = new Vault();
vm.deal(alice, 100 ether);
}

function test_DepositSuccess() public {
vm.prank(alice);
vault.deposit{value: 1 ether}();
assertEq(vault.balanceOf(alice), 1 ether);
}

function testFuzz_DepositWithdraw(uint96 amount) public {
vm.assume(amount > 0);
vm.assume(amount < 100 ether);

vm.startPrank(alice);
vault.deposit{value: amount}();
vault.withdraw(amount);
vm.stopPrank();

assertEq(vault.balanceOf(alice), 0);
}
}

运行:

forge test -vvvv

3. Invariant 测试:验证系统不变量

不变量示例:合约 ETH 余额 >= 所有用户余额之和

function invariant_BalanceNeverNegative() public {
assertGe(address(vault).balance, 0);
}

实际项目中常见不变量:

  • totalSupply == 全部账户余额之和
  • 抵押率永远高于清算线
  • 任何情况下都不能铸造负债资产

4. 主网分叉测试(Fork Test)

这是连接“本地代码”和“真实 DeFi 生态”的关键。

4.1 启动分叉节点

anvil --fork-url $BASE_MAINNET_RPC_URL

4.2 在测试里选择分叉

uint256 forkId = vm.createSelectFork(vm.envString("BASE_MAINNET_RPC_URL"));
assertGt(forkId, 0);

你可以在 fork 环境里直接与真实协议交互,例如:

  • 调用真实 ERC20
  • 调用 Uniswap 路由
  • 验证授权、滑点、回调逻辑

5. 与最新 Solidity 版本对齐

截至 2026-03,建议:

  • 在 CI 固定编译器版本,例如 0.8.34
  • 不要在生产流水线里使用“漂移版本”

foundry.toml 建议:

[profile.default]
solc_version = "0.8.34"
evm_version = "prague"
optimizer = true
optimizer_runs = 200
版本注意

Solidity 官方在 2026-02-18 发布了 0.8.34,修复了 0.8.28 ~ 0.8.33--via-ir 下清理 transient/persistent storage 的高危 bug。若你项目使用 --via-ir 且涉及 delete transient 变量,请尽快升级。


6. CI 最小模板

forge fmt --check
forge build
forge test -vvv
forge test --gas-report

如果有 fork 测试,再加:

forge test --match-path test/fork/*.t.sol -vvv

7. 实战建议

  • 每个业务模块至少包含:成功路径 + 失败路径 + 边界条件
  • 对关键资产路径必须写 invariant
  • 与外部协议交互必须写 fork 测试
  • 版本升级后,先跑 gas 快照再对比行为回归

8. 参考链接