Solidity 测试 Internal 函数 深度学习文档
基于 RareSkills 文章整理,面向有 Solidity 基础的中级开发者 参考来源:https://rareskills.io/post/solidity-test-internal-function
一、问题:为什么 Internal 函数难以测试?
1.1 Solidity 函数可见性回顾
| 可见性 | 外部可调用 | 子合约可调用 | 合约内可调用 |
|---|---|---|---|
public | ✅ | ✅ | ✅ |
external | ✅ | ❌(需 this) | ❌(需 this) |
internal | ❌ | ✅ | ✅ |
private | ❌ | ❌ | ✅ |
1.2 测试的困境
contract MyContract {
function _calculateReward(uint256 stake, uint256 duration) internal pure returns (uint256) {
return stake * duration / 365;
}
}
问题:
- 测试合约(如 Foundry Test)是外部调用者
internal函数不能被外部调用- 无法直接在测试中调用
myContract._calculateReward(100, 30)
1.3 为什么不直接改成 public?
- ❌ 污染合约的外部接口(ABI 暴露不必要的函数)
- ❌ 增加攻击面
- ❌ 违反封装原则(internal 意味着”不应该被外部直接调用”)
- ❌ 可能改变 Gas 开销(public 需要处理 calldata)
二、方法一:Harness 合约模式(推荐)
2.1 核心思路
创建一个继承目标合约的”测试包装合约”(Harness),将 internal 函数暴露为 public/external。
2.2 实现步骤
Step 1:原始合约
// src/RewardCalculator.sol
contract RewardCalculator {
mapping(address => uint256) public rewards;
function _calculateReward(uint256 stake, uint256 duration) internal pure returns (uint256) {
if (duration == 0) return 0;
return stake * duration / 365;
}
function _applyBonus(uint256 base, uint8 tier) internal pure returns (uint256) {
if (tier == 1) return base * 110 / 100;
if (tier == 2) return base * 125 / 100;
if (tier == 3) return base * 150 / 100;
return base;
}
}
Step 2:创建 Harness 合约
// test/harness/RewardCalculatorHarness.sol
import "../src/RewardCalculator.sol";
contract RewardCalculatorHarness is RewardCalculator {
// 暴露 internal 函数为 external
function exposed_calculateReward(uint256 stake, uint256 duration) external pure returns (uint256) {
return _calculateReward(stake, duration);
}
function exposed_applyBonus(uint256 base, uint8 tier) external pure returns (uint256) {
return _applyBonus(base, tier);
}
}
Step 3:在测试中使用 Harness
// test/RewardCalculator.t.sol
import "forge-std/Test.sol";
import "./harness/RewardCalculatorHarness.sol";
contract RewardCalculatorTest is Test {
RewardCalculatorHarness harness;
function setUp() public {
harness = new RewardCalculatorHarness();
}
function test_calculateReward_basic() public {
uint256 result = harness.exposed_calculateReward(365 ether, 365);
assertEq(result, 365 ether);
}
function test_calculateReward_zeroDuration() public {
uint256 result = harness.exposed_calculateReward(100 ether, 0);
assertEq(result, 0);
}
function test_applyBonus_tier2() public {
uint256 result = harness.exposed_applyBonus(100, 2);
assertEq(result, 125);
}
}
2.3 命名约定
| 约定 | 示例 |
|---|---|
前缀 exposed_ | exposed_calculateReward() |
前缀 harness_ | harness_calculateReward() |
前缀 test__(双下划线) | test__calculateReward() |
选一种统一使用即可,exposed_ 是社区最常见的。
三、方法二:测试合约直接继承
3.1 核心思路
让 Foundry 测试合约本身继承被测合约,这样测试合约就是子合约,可以直接调用 internal 函数。
3.2 实现
// test/RewardCalculator.t.sol
import "forge-std/Test.sol";
import "../src/RewardCalculator.sol";
contract RewardCalculatorTest is Test, RewardCalculator {
function test_calculateReward() public {
// 直接调用 internal 函数!因为 Test 合约继承了 RewardCalculator
uint256 result = _calculateReward(365 ether, 365);
assertEq(result, 365 ether);
}
function test_applyBonus() public {
uint256 result = _applyBonus(100, 3);
assertEq(result, 150);
}
}
3.3 优缺点
| 方面 | Harness 模式 | 直接继承模式 |
|---|---|---|
| 隔离性 | ✅ 测试和被测对象分离 | ⚠️ 测试合约就是被测合约 |
| constructor 冲突 | ✅ 不影响 | ⚠️ 可能有构造函数冲突 |
| 多重继承 | ✅ 清晰 | ⚠️ 钻石继承问题 |
| 代码量 | 多一个文件 | 更简洁 |
| 适用场景 | 复杂合约 | 简单/无状态合约 |
3.4 直接继承的问题
// ⚠️ 如果原合约有复杂的 constructor
contract MyToken is ERC20 {
constructor() ERC20("Token", "TKN") {
_mint(msg.sender, 1000000e18);
}
function _customLogic(uint x) internal pure returns (uint) {
return x * 2;
}
}
// 测试合约也必须调用父 constructor
contract MyTokenTest is Test, MyToken {
// 这里变得复杂...可能有意想不到的状态初始化
constructor() MyToken() {}
}
建议: 对于有状态的合约,优先使用 Harness 模式。
四、方法三:Abstract 合约的测试
4.1 问题
Abstract 合约不能直接部署,因为有未实现的函数:
abstract contract BaseVault {
function _getExchangeRate() internal view virtual returns (uint256);
function _convertToShares(uint256 assets) internal view returns (uint256) {
return assets * 1e18 / _getExchangeRate();
}
}
4.2 解决:实现 + 暴露
// test/harness/BaseVaultHarness.sol
contract BaseVaultHarness is BaseVault {
uint256 public mockRate = 1e18;
// 实现 abstract 函数(mock 版本)
function _getExchangeRate() internal view override returns (uint256) {
return mockRate;
}
// 暴露要测试的 internal 函数
function exposed_convertToShares(uint256 assets) external view returns (uint256) {
return _convertToShares(assets);
}
// 允许测试设置 mock 值
function setMockRate(uint256 rate) external {
mockRate = rate;
}
}
4.3 测试代码
contract BaseVaultTest is Test {
BaseVaultHarness vault;
function setUp() public {
vault = new BaseVaultHarness();
}
function test_convertToShares_1to1() public {
vault.setMockRate(1e18);
assertEq(vault.exposed_convertToShares(100e18), 100e18);
}
function test_convertToShares_2to1() public {
vault.setMockRate(2e18);
assertEq(vault.exposed_convertToShares(100e18), 50e18);
}
}
五、Private 函数的测试
5.1 Private 函数不能通过继承访问
contract MyContract {
function _privateHelper(uint x) private pure returns (uint) {
return x + 42;
}
}
contract Harness is MyContract {
function exposed_privateHelper(uint x) external pure returns (uint) {
// ❌ 编译错误!private 函数在子合约中不可见
return _privateHelper(x);
}
}
5.2 解决方案
| 方案 | 做法 | 适用情况 |
|---|---|---|
| 改为 internal | 将 private 改为 internal | 如果语义允许 |
| 通过 public 函数间接测试 | 测试调用 private 函数的 public 函数 | 大多数情况推荐 |
| 复制逻辑到 Harness | 在 Harness 中重写相同逻辑 | 不推荐(容易不同步) |
最佳实践: 如果一个函数值得单独测试,它大概率应该是 internal 而非 private。
六、Foundry 特有技巧
6.1 Fuzz Testing 与 Harness 结合
function testFuzz_calculateReward(uint256 stake, uint256 duration) public {
// 限制输入范围避免溢出
stake = bound(stake, 0, 1e30);
duration = bound(duration, 0, 3650);
uint256 result = harness.exposed_calculateReward(stake, duration);
// 属性测试
if (duration == 0) {
assertEq(result, 0);
} else {
assertLe(result, stake); // 奖励不应超过本金(duration < 365 时)
}
}
6.2 测试带状态的 Internal 函数
contract StatefulContract {
uint256 internal totalDeposits;
function _recordDeposit(address user, uint256 amount) internal {
totalDeposits += amount;
}
}
contract StatefulHarness is StatefulContract {
function exposed_recordDeposit(address user, uint256 amount) external {
_recordDeposit(user, amount);
}
function getTotalDeposits() external view returns (uint256) {
return totalDeposits;
}
}
七、项目文件结构建议
project/
├── src/
│ ├── RewardCalculator.sol
│ └── BaseVault.sol
├── test/
│ ├── RewardCalculator.t.sol
│ ├── BaseVault.t.sol
│ └── harness/ ← Harness 合约放在这里
│ ├── RewardCalculatorHarness.sol
│ └── BaseVaultHarness.sol
注意: Harness 合约只用于测试,绝对不要部署到生产环境。
八、关键概念回顾
- Internal 函数不可外部调用 — 这是测试它们的根本困难
- Harness 模式(推荐) — 继承 + 暴露为 external,最清晰安全
- 直接继承模式 — 简单场景可用,复杂合约可能有 constructor 冲突
- Abstract 合约测试 — 需要 mock 实现 + 暴露 internal
- Private 无法继承 — 建议改为 internal 或通过 public 接口间接测试
- 不要为了测试改 public — 污染接口、增加攻击面
- Harness 仅用于测试 — 永远不要部署到主网
文档整理日期:2026-05-31 参考来源:RareSkills - How to Test Internal Functions in Solidity