# Foundry 本地合约测试：验证成功写入和失败回滚

整理：链小知（AI 辅助）
资料核对日期：2026-10-11
引用地址：https://lianxiaozhi.com/tools/guides/foundry-local-counter-revert-test
难度：进阶
核验方式：官方文档整理，未执行真实账户操作

用独立的计数器例子在本地验证写入、revert 和断言失败。完整流程不需要私钥、公共 RPC 或广播交易。

## 使用工具
- [Foundry](https://lianxiaozhi.com/tools/foundry) · [官方入口](https://github.com/foundry-rs/foundry)

## 前置条件
- 已安装可用 Foundry，能够运行 forge；安装方式以官方仓库当前说明为准。
- 使用一个新的本地练习目录，不放入生产私钥或钱包助记词。此教程按官方文档编写，未声称已替读者运行测试。

## 1. 建立独立练习项目

在空目录中运行 forge init tutorial-counter，再进入该目录。官方仓库说明 Forge 提供构建与测试工具，Anvil 则是本地节点；此处的单元测试不需要额外启动节点。

检查项目已包含 foundry.toml 和 forge-std。若初始化失败，先保存错误并检查目录冲突与依赖获取情况，不继续对生产仓库执行安装或覆盖。

核对结果：练习项目位于新目录，且能够定位 forge-std/Test.sol。
依据：[Foundry 官方仓库：Forge 初始化、构建与测试](https://github.com/foundry-rs/foundry)
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 2. 写入可控的测试合约

在 test/BoundedCounter.t.sol 保存下列独立例子。setNumber 只接受小于 10 的值，方便验证成功路径及失败后状态保留。

pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";

contract BoundedCounter {
    uint256 public number;
    function setNumber(uint256 next) external {
        require(next < 10, "limit");
        number = next;
    }
}

contract BoundedCounterTest is Test {
    function test_SetAndReject() public {
        BoundedCounter counter = new BoundedCounter();
        counter.setNumber(7);
        assertEq(counter.number(), 7);
        vm.expectRevert();
        counter.setNumber(10);
        assertEq(counter.number(), 7);
    }
}

这段代码是本教程自编示例；Test、assertEq 与 vm.expectRevert 的使用依据官方 forge-std 文档。示例只检查指定输入与回滚，不代表完成合约审计。

核对结果：文件包含一个合约和一个继承 Test 的测试合约，没有 RPC、私钥或广播设置。
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 3. 先构建，解决编译问题

运行 forge build。若报 pragma 版本不匹配，检查 foundry.toml 的编译器设置；若 Test.sol 未找到，检查依赖目录和 remapping，而不是删除 import 跳过测试。

构建成功只说明编译通过，尚未证明断言成立。保留构建结果，再进入测试步骤。

核对结果：构建没有编译错误，且测试文件实际纳入项目。
依据：[Foundry 官方仓库：Forge 初始化、构建与测试](https://github.com/foundry-rs/foundry)
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 4. 运行指定测试并读出断言含义

运行 forge test --match-contract BoundedCounterTest -vv。测试先设置 7 并读取确认，然后期待设置 10 被拒绝，最后再次确认状态仍为 7。

这分别验证合法输入写入、非法输入 revert，以及失败没有覆盖原状态。看到成功数量时还要确认列出的测试名称确实是 test_SetAndReject；运行了其他默认测试不足以证明本例通过。

核对结果：输出显示本例被执行且通过；保存测试名称与结果。
依据：[Foundry 官方仓库：Forge 初始化、构建与测试](https://github.com/foundry-rs/foundry)
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 5. 主动让断言失败，确认测试会发现错误

把最后一条 assertEq 的预期值临时改成 8，重新运行相同命令。此时应该出现失败断言，因为状态仍为 7。

如果改错预期仍全部通过，先检查保存位置、筛选的合约名与实际运行文件。这个检查避免只看测试进程退出而没有验证目标例子。

核对结果：错误预期导致本例失败，输出能定位到断言；不是通过删测试得到成功。
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 6. 恢复例子并说明验证范围

将最后一条预期恢复为 7，重跑测试并保留结果。整个流程使用本地测试执行，不需要 forge script --broadcast、cast send 或钱包签名。

完成记录注明编译器、测试名称、正常通过和故意失败两组结果。它证明这一小段逻辑在指定条件下的行为，不证明部署地址、复杂资产流或主网交易安全。

核对结果：恢复后通过，并有一次受控失败作为测试确实生效的证据。
依据：[Foundry 官方仓库：Forge 初始化、构建与测试](https://github.com/foundry-rs/foundry)
依据：[Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)

## 常见错误
- 只运行 forge build 就声称行为测试通过。
- 测试筛选错对象，默认示例通过而自己的文件没有执行。
- 为了让结果通过删除断言或 expectRevert。
- 在练习项目中加入生产私钥、公共链广播或资产授权。

## 完成检查
- 完成合法写入与非法输入回滚两条路径的验证。
- 一次故意改错预期能被测试发现，恢复后再次通过。
- 记录验证范围为本地示例测试，不扩大为生产安全保证。

## 相关知识
- [Gas 怎么计算？交易失败为什么仍可能扣费](https://lianxiaozhi.com/learn/gas-and-failed-transactions)

## 官方依据
- [Foundry 官方仓库：Forge 初始化、构建与测试](https://github.com/foundry-rs/foundry)
- [Forge Standard Library：Test、断言与 expectRevert](https://github.com/foundry-rs/forge-std)
