CounterDemo1 合约测试说明文档(含完整测试代码)
本文档将
CounterDemo1Test.sol测试合约源码与其详细说明整合,便于开发者在阅读代码的同时理解设计意图、测试逻辑与最佳实践。适用于开发、测试及安全审计场景。 作者:RainWeb3 更新时间:2025年11月11日
国外可访问:rainweb3知识库
国内可访问:rainweb3知识库
📚 目录
1. 概述
本测试套件基于 Foundry 框架编写,用于全面验证 CounterDemo1.sol 合约的功能正确性、权限控制、边界条件处理以及抗溢出能力。测试覆盖:
- 正常流程
- 权限限制
- 异常回退(含错误消息)
- 上/下溢防护
- 模糊测试(随机输入)
2. 完整测试代码
合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
/**
* @title CounterDemo1
* @author RainWeb3
* @notice A simple counter contract with increment and decrement functionality
* 计数器合约,包含增减功能
*/
contract CounterDemo1 {
/**
* @notice State variable to store the count value
* 存储计数值的状态变量
*
* public 关键字表示该变量可以被合约外部访问
* uint256 表示无符号整数类型,表示非负整数
*
* count is used to track the current count value
* count 变量用于跟踪当前的计数值
*
* owner variable stores the address of the contract owner
* owner 变量存储合约拥有者的地址
*/
uint256 public count;
address public owner;
/**
* @dev Constructor to initialize the contract owner
* 构造函数初始化合约拥有者
*/
constructor() {
owner = msg.sender;
}
/**
* @dev Custom error for unauthorized access
* 自定义错误,表示未经授权的访问
*
* 节省gas原因:自定义错误信息不存储在字节码中,只用 4 字节的 selector 表示。
* 相比之下,require 语句中的错误字符串会被存储在字节码中,占用更多的空间和 gas。
* 缺点需要结合前端解析 ABI进行调试,无法直接从链上查看错误信息
*/
error OnlyAccessibleToTheOwner();
/**
* @dev Internal function to check if the caller is the owner
* 内部函数检查调用者是否为拥有者
*/
function _onlyOwner() internal view {
if (msg.sender != owner) {
revert OnlyAccessibleToTheOwner();
}
}
/**
* @dev Modifier to restrict access to only the contract owner
* 限制只有合约拥有者可以访问被修饰的函数
*
* _; 表示继续执行被修饰的函数
*/
modifier onlyOwner() {
_onlyOwner();
_;
}
/**
* @dev Internal function to check for underflow condition
* 内部函数检查下溢条件
*
* 注意: 0.8.0 版本及以上,Solidity 默认启用整数溢出和下溢检查
* 因此,手动检查下溢条件不是必需的,但这里为了演示修饰符的使用
*
* 即便封装了require错误消息,仍然比自定义错误消耗gas更多,但更易于调试
* (生产开发,优先推荐使用自定义错误)
*/
function _checkForUnderflow() internal view {
require(count > 0, "Count cannot be negative");
}
/**
* @dev Modifier to check for underflow before decrementing the count
* 在减少计数值之前检查下溢条件
*
* _; 表示继续执行被修饰的函数
*/
modifier checkForUnderflow() {
_checkForUnderflow();
_;
}
/**
* @dev Function to get the current count value
* 获取当前计数值
*
* @notice Returns the current count value
* 注意: 返回当前的计数值
*
* @return uint256 The current count value
* 返回当前的计数值
*
* public 关键字表示该函数可以被合约外部访问
* view 函数表示该函数不会修改区块链上的状态
* returns 关键字用于指定函数的返回值类型
* uint256 表示返回值类型为无符号整数
* function get() 返回当前的计数值
*/
function get() public view returns (uint256) {
return count;
}
/**
* @dev Increment the count value by 1
* 增加计数值1
*
* @notice This function can only be called by the contract owner
* 注意: 该函数只能由合约拥有者调用
*
* @return uint256 The updated count value after incrementing
* 返回增加后的更新计数值
*
* The function uses the onlyOwner modifier to restrict access
* 该函数使用 onlyOwner 修饰符以限制访问
*
* public 关键字表示该函数可以被合约外部访问
* pure 函数表示该函数不会读取或修改合约的状态变量
* returns 关键字用于指定函数的返回值类型
* uint256 表示返回值类型为无符号整数
* onlyOwner 修饰符限制只有合约拥有者可以调用该函数
*/
function inc() public onlyOwner returns (uint256) {
return count += 1;
}
/**
* @dev Decrement the count value by 1
* 减少计数值1
*
* @notice This function will revert if the count is already 0
* 注意: 如果计数值已经是0,调用该函数将会回滚
*
* @return uint256 The updated count value after decrementing
* 返回减少后的更新计数值
*
* The function uses the onlyOwner modifier to restrict access
* 该函数使用 onlyOwner 修饰符以限制访问
*
* The function uses the checkForUnderflow modifier to prevent underflow
* 该函数使用 checkForUnderflow 修饰符以防止下溢
*
* Modifier checkForUnderflow checks if count is greater than 0 before decrementing
* checkForUnderflow 修饰符在减少之前检查 count 是否大于0
*
* public 关键字表示该函数可以被合约外部访问
* pure 函数表示该函数不会读取或修改合约的状态变量
* returns 关键字用于指定函数的返回值类型
* uint256 表示返回值类型为无符号整数
* onlyOwner 修饰符限制只有合约拥有者可以调用该函数
* checkForUnderflow 修饰符防止下溢
*/
function dec() public onlyOwner checkForUnderflow returns (uint256) {
return count -= 1;
}
}
---------------------------------------------------------
测试代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {CounterDemo1} from "src/CounterDemo1.sol";
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
contract CounterDemo1Test is Test {
/**
* @notice Instance of the CounterDemo1 contract to be tested
注意: 这里声明了一个 CounterDemo1 合约的实例变量 counter1,用于在测试中调用合约的函数
*/
CounterDemo1 public counter1;
/**
* @notice Predefined owner address for testing
* 注意: 这里预定义了一个地址作为合约拥有者地址,用于测试目的
* 在实际测试中,建议通过 vm.prank 设置合约拥有者地址,以确保测试的隔离性和准确性
*
* 不建议使用下面的硬编码地址,推荐使用 vm.prank(owner) 设置调用者地址
*/
// address public owner = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
address public owner;
/**
* @dev setUp function is called before each test case
* setUp() 函数在每个测试之前部署 Counter1 合约
* 注意: 所有vm调用的,都无法全局使用。
* vm 是一个合约接口(通常是 Vm 或 VmSafe),vm.xxx() 本质是 对外部合约的函数调用;
* Solidity 禁止在全局/状态变量初始化时进行函数调用(即使是内部函数也不行,除非是纯常量表达式)
*/
function setUp() public {
/**
* 方式1:推荐
* 通过环境变量获取合约拥有私钥对应的地址
* 获取环境变量字符串,转换为 uint256 类型的私钥,然后通过私钥生成地址
* 设置部署者合约地址
*/
// string memory pkStr = vm.envString("LOCAL_PRIVATE_KEY");
// uint256 privateKey = vm.parseUint(pkStr); // ✅ 字符串 → uint256
// owner = vm.addr(privateKey);
owner = vm.addr(vm.parseUint(vm.envString("LOCAL_PRIVATE_KEY")));
vm.prank(owner);
/**
* 方式2:不推荐
* 指定上面的全局部署者地址
*/
// vm.prank(owner);
/**
* @dev Deploy a new instance of the Counter1 contract before each test
* 这里使用 new 关键字部署 Counter1 合约的新实例
*/
counter1 = new CounterDemo1();
}
/**
* @dev _asOwner function to simulate actions as the contract owner
* 模拟以合约拥有者身份执行操作
*/
function _asOwner() internal {
vm.startPrank(owner);
}
// ======================== 普通测试 ======================================================
/**
* @dev Test function to verify the initial count value
* testCount() 函数用于测试初始计数值是否为0
*/
function testCount() public view {
uint256 initialCount = counter1.count();
console2.log("Initial count is:", initialCount);
uint256 currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
assertEq(initialCount, currentNumber);
}
/**
* @dev Test function to verify the get() function
* testGet() 函数用于测试 get() 函数的返回值是否正确
*/
function testGet() public view {
uint256 currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
assertEq(currentNumber, 0);
}
/**
* @dev Test function to verify that only the contract owner can call inc() and dec()
* 验证只有合约拥有者才能调用 inc() 和 dec() 函数
*
* This test ensures that non-owner accounts are properly reverted with the
* custom error `OnlyAccessibleToTheOwner` when attempting to modify the counter.
* 本测试确保非拥有者账户在尝试修改计数器时,
* 会因自定义错误 `OnlyAccessibleToTheOwner` 而正确回退。
*
* The test calls `inc()` and `dec()` without using `vm.prank(owner)`,
* so `msg.sender` is the default test address (not the owner).
* 测试在未使用 `vm.prank(owner)` 的情况下调用 `inc()` 和 `dec()`,
* 因此 `msg.sender` 为默认测试地址(非 owner)。
*/
function testOnlyOwner() public {
vm.expectRevert(CounterDemo1.OnlyAccessibleToTheOwner.selector);
counter1.inc();
vm.expectRevert(CounterDemo1.OnlyAccessibleToTheOwner.selector);
counter1.dec();
}
/**
* @dev Test function to verify the inc() function
* testInc() 函数用于测试 inc() 函数是否正确增加计数值
*/
function testInc() public {
_asOwner();
uint256 addNumer = counter1.inc();
console2.log("After increment, number is:", addNumer);
vm.stopPrank();
uint256 currentNumber = counter1.get();
console2.log("Current number from get() is:", currentNumber);
assertEq(addNumer, currentNumber);
}
/**
* @dev Test function to verify the dec() function
* testDec() 函数用于测试 dec() 函数是否正确减少计数值
*/
function testDec() public {
uint256 currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
_asOwner();
counter1.inc();
uint256 decNumber = counter1.dec();
console2.log("After decrement, number is:", decNumber);
vm.stopPrank();
currentNumber = counter1.get();
console2.log("Current number from get() is:", currentNumber);
assertEq(decNumber, currentNumber);
}
// ======================== 异常测试 ======================================================
/**
* @dev Test function to verify the underflow behavior of the dec() function
* testUnderflow1() 函数用于测试 dec() 函数的下溢行为
* 当计数值为0时,调用 dec() 函数应触发 "Count cannot be negative" 错误信息
* 消息一致时,测试将通过
*/
function testUnderflow1() public {
uint256 currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
console2.log(
"Expecting revert with message 'Count cannot be negative'"
);
_asOwner();
vm.expectRevert("Count cannot be negative");
counter1.dec();
}
/**
* @dev Test function to verify the underflow behavior of the dec() function
* testUnderflow2() 函数用于测试 dec() 函数的下溢行为
*/
function testUnderflow2() public {
uint256 currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
_asOwner();
uint256 numberInc = counter1.inc();
console2.log("After increment, number is:", numberInc);
currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
assertEq(numberInc, currentNumber);
uint256 numberDec = counter1.dec();
console2.log("After decrement, number is:", numberDec);
currentNumber = counter1.get();
console2.log("Current number is:", currentNumber);
assertEq(numberDec, currentNumber);
vm.expectRevert("Count cannot be negative");
numberDec = counter1.dec();
}
/**
* @dev Test function to verify overflow behavior of the inc() function
* testIncOverflow() 函数用于测试 inc() 函数的溢出行为
* 当计数值达到 uint256 的最大值时,调用 inc() 函数应触发溢出错误
* 但由于 Solidity 0.8.x 版本默认启用溢出检查,测试将通过
* 这里通过直接操作存储槽来模拟计数值达到最大值的情况
* 避免了实际循环带来的高gas消耗和时间问题
* vm.store 用于直接修改合约的存储槽
* bytes32(0) 表示第一个存储槽,对应于 count 变量
* bytes32(type(uint256).max) 将 count 变量设置为 uint256 的最大值
* 然后调用 inc() 函数,预期会触发溢出错误
* 这种方法更高效且实用 (推荐)
*/
function testIncOverflow() public {
/**
* @dev Note: Directly manipulating storage slots to simulate overflow condition
* 备注: 直接操作存储槽以模拟溢出条件
* In practice, we would not loop to the max value
* 实际上,我们不会循环到最大值
* but Solidity 0.8+ guarantees overflow reverts.
* 但 Solidity 0.8+ 保证溢出会回退。
* To explicitly verify, we use storage manipulation:
* 为了明确验证,我们使用存储操作
*/
vm.store(address(counter1), bytes32(0), bytes32(type(uint256).max));
_asOwner();
vm.expectRevert();
counter1.inc();
vm.stopPrank();
}
// ======================== 模糊测试 ======================================================
/**
* @dev Fuzz test function to verify inc() function with various inputs
* testFuzz_IncNumber() 函数用于模糊测试 inc() 函数
* 通过传入不同的 n 值,测试 inc() 函数是否正确增加计数值
* 使用 vm.assume 限制 n 的范围,避免过大导致测试时间过长和溢出
*/
function testFuzz_IncNumber(uint256 number) public {
vm.assume(number < 1000);
_asOwner();
for (uint256 i = 0; i < number; i++) {
counter1.inc();
}
vm.stopPrank();
assertEq(counter1.get(), number);
}
/**
* @dev Fuzz test function to verify combined inc() and dec() functions with various inputs
* testFuzz_IncAndDec() 函数用于模糊测试 inc() 和 dec() 函数的联合操作
* 通过传入不同的 n 值,测试 inc() 和 dec() 函数是否正确增加和减少计数值
* 使用 vm.assume 限制 n 的范围,避免过大导致测试时间过长和溢出
*/
function testFuzz_IncAndDec(uint256 number) public {
vm.assume(number < 1000);
uint256 start = counter1.get();
_asOwner();
for (uint256 i = 0; i < number; i++) {
counter1.inc();
}
assertEq(counter1.get(), start + number);
for (uint256 i = 0; i < number; i++) {
counter1.dec();
}
vm.stopPrank();
assertEq(counter1.get(), start);
}
/**
* @dev Fuzz test function to verify dec() function only allowed when count is positive
* testFuzz_DecOnlyAllowedWhenPositive() 函数用于模糊测试 dec() 函数
* 确保只有在计数值为正时才能调用 dec() 函数
* 通过传入不同的初始计数值,测试 dec() 函数的行为
* 使用 vm.assume 限制初始计数值的范围,避免过大导致测试时间过长和溢出
*/
function testFuzz_DecOnlyAllowedWhenPositive(uint256 initialCount) public {
vm.assume(initialCount <= 1000);
_asOwner();
for (uint256 i = 0; i < initialCount; i++) {
counter1.inc();
}
console2.log("Initial count set to:", counter1.get());
for (uint256 i = 0; i < initialCount; i++) {
counter1.dec();
}
console2.log("Count after decrements:", counter1.get());
for (uint256 i = 0; counter1.get() != 0; i++) {
counter1.dec();
}
vm.expectRevert("Count cannot be negative");
counter1.dec();
}
}
3. 测试环境配置
3.1 部署者地址设置
owner = vm.addr(vm.parseUint(vm.envString("LOCAL_PRIVATE_KEY")));
- 作用:动态从环境变量
LOCAL_PRIVATE_KEY读取私钥,生成 owner 地址。 - 优势:避免硬编码地址,提升测试可移植性与安全性。
- 要求:运行前需在
.env文件中配置:LOCAL_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
3.2 setUp 函数说明
- 执行时机:每个测试用例前自动调用。
- 核心逻辑:
- 设置 owner 地址。
- 使用
vm.prank(owner)使后续部署由 owner 发起。 - 部署新
CounterDemo1实例。
- 注意:
vm.xxx()是对外部合约的调用,不能用于状态变量初始化。
4. 普通功能测试
4.1 testCount:验证初始计数值一致性
- 目的:确认
count()与get()初始值一致(均为 0)。 - 日志输出:便于调试观察初始状态。
4.2 testGet:验证 get() 返回值
- 类型:
view测试,无状态变更。 - 断言:
assertEq(currentNumber, 0)
4.3 testOnlyOwner:权限控制验证
- 机制:
- 默认调用者 ≠ owner。
- 使用
vm.expectRevert(selector)精确匹配自定义错误OnlyAccessibleToTheOwner。
- 安全意义:防止未授权修改。
4.4 testInc:验证 inc() 功能
- 流程:
_asOwner()切换调用者。- 调用
inc()并捕获返回值。 - 对比
get()结果。
- 断言:返回值与当前状态一致。
4.5 testDec:验证 dec() 功能
- 关键点:先
inc()使计数 > 0,再dec()。 - 避免:对 0 值直接
dec()(会回退)。
5. 异常行为测试
5.1 testUnderflow1 / testUnderflow2:下溢保护验证
- 触发条件:
count == 0时调用dec()。 - 预期:回退并抛出
"Count cannot be negative"。 - 验证方式:
vm.expectRevert("Count cannot be negative")
5.2 testIncOverflow:上溢保护验证
- 挑战:无法循环至
uint256.max。 - 解决方案:
vm.store(address, bytes32(0), bytes32(max))直接写存储。 - 验证:调用
inc()应因算术溢出回退(Solidity 0.8+ 默认开启)。
6. 模糊测试(Fuzz Testing)
Foundry 自动对带参数测试函数进行多轮随机输入。
6.1 testFuzz_IncNumber:多次递增测试
- 约束:
vm.assume(number < 1000) - 断言:最终
get() == number
6.2 testFuzz_IncAndDec:递增后递减回归测试
- 目标:验证“增 N 减 N”后状态恢复。
- 断言:最终
get() == 初始值
6.3 testFuzz_DecOnlyAllowedWhenPositive:边界递减测试
- 流程:
- 设置任意初始正数。
- 递减至 0。
- 再次
dec()应触发下溢错误。
- 覆盖:任意正数归零后的防护。
7. 最佳实践与注意事项
| 类别 | 建议 |
|---|---|
| ✅ 推荐做法 | 使用环境变量管理私钥;用 vm.store 模拟极端状态;模糊测试加 vm.assume 限制范围 |
| ⚠️ 避免做法 | 全局变量中调用 vm.xxx();循环至 uint256.max;忽略错误消息验证 |
| 🔒 安全建议 | 所有状态变更函数必须有权限控制;依赖 Solidity 0.8+ 溢出保护或显式检查 |
| 🧪 测试完整性 | 覆盖正常路径、异常路径、边界条件、权限绕过 |
📌 提示:运行
forge test -vvv可查看详细日志,辅助调试。
✅ 本测试套件已全面覆盖 CounterDemo1 的功能与安全边界,符合 Foundry 测试最佳实践。