简单合约测试说明文档(含完整测试代码)

50 阅读14分钟

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 函数说明

  • 执行时机:每个测试用例前自动调用。
  • 核心逻辑
    1. 设置 owner 地址。
    2. 使用 vm.prank(owner) 使后续部署由 owner 发起。
    3. 部署新 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() 功能

  • 流程
    1. _asOwner() 切换调用者。
    2. 调用 inc() 并捕获返回值。
    3. 对比 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:边界递减测试

  • 流程
    1. 设置任意初始正数。
    2. 递减至 0。
    3. 再次 dec() 应触发下溢错误。
  • 覆盖:任意正数归零后的防护。

7. 最佳实践与注意事项

类别建议
推荐做法使用环境变量管理私钥;用 vm.store 模拟极端状态;模糊测试加 vm.assume 限制范围
⚠️ 避免做法全局变量中调用 vm.xxx();循环至 uint256.max;忽略错误消息验证
🔒 安全建议所有状态变更函数必须有权限控制;依赖 Solidity 0.8+ 溢出保护或显式检查
🧪 测试完整性覆盖正常路径、异常路径、边界条件、权限绕过

📌 提示:运行 forge test -vvv 可查看详细日志,辅助调试。


✅ 本测试套件已全面覆盖 CounterDemo1 的功能与安全边界,符合 Foundry 测试最佳实践。