Foundry测试实战:解锁区块链测试新姿势
1.普通测试实战
环境搭建与项目初始化
在开始使用 Foundry 进行普通测试实战之前,首先需要搭建好开发环境并初始化项目。以 Ubuntu 系统为例,安装 Foundry 的步骤如下:
-
安装依赖:
sudo apt install curl git
-
安装 Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundry
如果在安装过程中遇到 443 拒绝访问的问题,可能是 GitHub 的 raw 域名被屏蔽。解决方法是修改 hosts 文件:
sudo vi /etc/hosts
# 添加以下内容
185.199.111.133 raw.githubusercontent.com
安装完成后,可以使用forge --version命令验证是否安装成功。
接下来初始化一个新项目,使用以下命令:forge init my_project --no - git
--no - git
参数表示不初始化 Git 仓库。初始化完成后,项目目录结构如下:
my_project
├── README.md
├── foundry.toml
├── lib
├── script
├── src
└── test
其中,src目录用于存放智能合约源码,test目录用于存放测试脚本,foundry.toml是项目的配置文件。
编写普通测试用例,假设我们有一个简单的智能合约Counter.sol,代码如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;contract Counter {uint256 public count;constructor() {count = 0;}function increment() public {count++;}function decrement() public {require(count > 0, "Count cannot be negative");count--;}
}
在test目录下创建测试脚本CounterTest.t.sol,编写测试用例:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;import "forge - std/Test.sol";
import "../src/Counter.sol";contract CounterTest is Test {Counter counter;function setUp() public {counter = new Counter();}function testInitialCount() public {assertEq(counter.count(), 0);}function testIncrement() public {counter.increment();assertEq(counter.count(), 1);}function testDecrement() public {counter.increment();counter.decrement();assertEq(counter.count(), 0);}function testDecrementFromZero() public {vm.expectRevert("Count cannot be negative");counter.decrement();}
}
在上述测试脚本中:
- setUp函数会在每个测试函数执行前被调用,用于初始化测试环境,这里是创建一个Counter合约实例。
- 以test开头的函数是测试用例,assertEq用于断言两个值是否相等,vm.expectRevert用于断言函数调用会触发指定的错误。
运行测试
运行测试非常简单,在项目根目录下执行以下命令:forge test
该命令会自动编译合约和测试脚本,并运行所有测试用例。如果只想运行某个特定合约的测试,可以使用--match - contract
参数:
forge test --match - contract CounterTest
如果只想运行某个测试用例,可以使用–match - test参数:
forge test --match - contract CounterTest --match - test testIncrement
运行测试后,会输出详细的测试结果,包括每个测试用例的执行状态、执行时间等信息。如果测试用例通过,会显示ok;如果测试用例失败,会显示失败的原因和相关的堆栈信息,方便开发者定位问题。
2. 分叉测试实战
分叉测试是一种在区块链测试中非常强大的技术,它允许开发者在本地模拟主网或其他网络的状态,从而在真实的环境中进行智能合约的测试 。通过分叉测试,我们可以测试智能合约与主网交互的各种场景,比如查询主网的余额、调用主网的其他合约等,而无需担心对真实的主网产生影响。这对于确保智能合约在实际运行环境中的正确性和稳定性至关重要,能够提前发现潜在的问题,避免在主网上线后出现意外情况。
首先,需要在foundry.toml文件中配置网络,以便进行分叉测试。假设我们要分叉以太坊主网,配置如下:
[profile.default]
src = "src"
out = "out"
libs = ["lib"][networks]
[networks.mainnet]
url = "https://eth - mainnet.alchemyapi.io/v2/your_api_key"
请将your_api_key
替换为你自己的 Alchemy API Key
。如果没有 Alchemy
账号,可以注册一个并获取 API Key。
接下来,在 Solidity 测试代码中进行分叉测试的操作。假设我们有一个合约PriceOracle.sol
,它需要从主网的某个价格预言机合约获取价格信息,测试脚本PriceOracleTest.t.sol
如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;import "forge - std/Test.sol";
import "../src/PriceOracle.sol";contract PriceOracleTest is Test {PriceOracle priceOracle;function setUp() public {vm.createSelectFork("mainnet");priceOracle = new PriceOracle();}function testGetPrice() public {uint256 price = priceOracle.getPrice();console.logUint(price);assert(price > 0);}
}
在上述代码中:
- vm.createSelectFork(“mainnet”)表示创建一个以太坊主网的分叉,这样后续的操作就会在这个分叉的网络状态上进行。
- priceOracle.getPrice()调用合约中的获取价格函数,console.logUint用于打印价格信息,assert(price > 0)断言获取到的价格大于 0。
运行分叉测试的命令与普通测试相同:forge test --match - contract PriceOracleTest
通过分叉测试,我们可以在本地模拟真实主网的环境,对依赖主网数据的智能合约进行全面的测试,确保其在实际运行中的可靠性。
3. 模糊测试实战
模糊测试(Fuzz Testing)是一种软件测试技术,它通过向目标系统提供随机的、畸形的或边界值的输入数据,来检测系统是否存在漏洞、崩溃或其他异常行为。在区块链智能合约测试中,模糊测试尤为重要,因为智能合约处理的是数字资产和重要业务逻辑,一旦存在漏洞,可能导致严重的安全问题和经济损失。Foundry 框架内置了强大的模糊测试功能,能够自动生成各种随机输入,对智能合约进行全面的测试,发现潜在的问题,这些问题可能是在普通测试中难以发现的,从而提高智能合约的安全性和稳定性。
假设我们有一个简单的数学运算合约MathContract.sol,代码如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;contract MathContract {function add(uint256 a, uint256 b) public pure returns (uint256) {return a + b;}function subtract(uint256 a, uint256 b) public pure returns (uint256) {require(a >= b, "Subtraction result cannot be negative");return a - b;}
}
接下来编写模糊测试用例,在test
目录下创建MathContractTest.t.sol
:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;import "forge - std/Test.sol";
import "../src/MathContract.sol";contract MathContractTest is Test {MathContract mathContract;function setUp() public {mathContract = new MathContract();}function testFuzzAdd(uint256 a, uint256 b) public {uint256 result = mathContract.add(a, b);assertEq(result, a + b);}function testFuzzSubtract(uint256 a, uint256 b) public {if (a >= b) {uint256 result = mathContract.subtract(a, b);assertEq(result, a - b);} else {vm.expectRevert("Subtraction result cannot be negative");mathContract.subtract(a, b);}}
}
在上述模糊测试代码中:
- 模糊测试函数以testFuzz开头,后面跟着具体的测试场景名称。
- 函数参数uint256 a和uint256 b会由 Foundry 自动生成随机值进行测试。
- 在testFuzzSubtract函数中,根据a和b的大小关系,分别测试正常情况和触发require revert 的情况。
运行模糊测试:forge test --match - contract MathContractTest
与普通测试相比,模糊测试的结果可能更加丰富和复杂。普通测试使用固定的输入值,只能验证特定情况下合约的正确性。而模糊测试通过大量的随机输入,能够覆盖更多的边界情况和异常情况。例如,在testFuzzSubtract函数中,普通测试可能只验证了a大于b的正常情况,而模糊测试可能会生成a小于b的情况,从而发现require语句是否正确工作。如果模糊测试发现问题,会在测试结果中显示具体的输入值和错误信息,帮助开发者快速定位和修复漏洞。通过模糊测试,我们可以更加全面地验证智能合约的健壮性,提高其在各种情况下的可靠性。
4. 不变性测试实战
不变性测试是一种用于验证智能合约在一系列操作前后,其状态的某些属性始终保持不变的测试方法。在区块链智能合约中,很多时候我们期望合约的某些状态变量或属性在不同的操作过程中遵循特定的规则,不会被意外修改或破坏。例如,在一个代币合约中,总供应量在任何情况下都应该保持不变(除了有明确的增发或销毁逻辑),这种期望的属性就是一种不变性。不变性测试通过对合约进行多次不同操作的组合,并在每次操作后检查这些关键属性,确保合约的行为符合预期,从而增强合约的可靠性和稳定性,防止因错误操作或漏洞导致的状态不一致问题。
首先,在foundry.toml文件中配置不变性测试相关参数,例如:
[profile.default]
src = "src"
out = "out"
libs = ["lib"][experimental]
invariant_testing = true
这里启用了invariant_testing
实验性功能。
假设我们有一个简单的银行账户合约BankAccount.sol
,代码如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;contract BankAccount {uint256 public balance;constructor(uint256 initialBalance) {balance = initialBalance;}function deposit(uint256 amount) public {balance += amount;}function withdraw(uint256 amount) public {require(balance >= amount, "Insufficient balance");balance -= amount;}
}
接下来编写不变性测试代码,在test目录下创建BankAccountTest.t.sol:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;import "forge - std/Test.sol";
import "../src/BankAccount.sol";contract BankAccountTest is Test {BankAccount bankAccount;function setUp() public {bankAccount = new BankAccount(100);}function invariant_balanceNonNegative() public view {assert(bankAccount.balance >= 0);}function invariant_totalBalance() public view {// 假设没有其他影响总余额的操作,这里简单验证余额不变assert(bankAccount.balance == 100);}function testDeposit() public {bankAccount.deposit(50);assertEq(bankAccount.balance, 150);}function testWithdraw() public {bankAccount.withdraw(30);assertEq(bankAccount.balance, 70);}
}
在上述代码中:
- invariant_balanceNonNegative和invariant_totalBalance函数是不变性测试函数,分别验证账户余额始终非负和总余额在没有特殊操作下保持不变。
- testDeposit和testWithdraw是普通测试函数,用于验证存款和取款操作的正确性。
运行不变性测试:forge test --match - contract BankAccountTest
如果不变性测试失败,会在测试结果中显示具体的错误信息,指出是哪个不变性条件被破坏,以及在哪个操作步骤后出现问题,帮助开发者快速定位和解决合约中的潜在问题,确保合约状态的稳定性和可靠性。
5. 差异化测试实战
差异化测试主要用于对比不同版本或不同实现之间的差异,确保在代码更新、重构或采用新的算法时,智能合约的功能和行为保持一致,并且没有引入新的问题。在区块链开发中,随着项目的演进,智能合约可能会经历多次修改和优化,例如升级合约的功能、改进算法、修复漏洞等。通过差异化测试,可以将新版本的合约与旧版本或参考实现进行对比,验证新的改动是否符合预期,是否对已有的功能产生负面影响,以及是否在新的实现中引入了兼容性问题或新的漏洞。这种测试方法能够帮助开发者及时发现并解决潜在的问题,保证智能合约在不同阶段的稳定性和可靠性,为用户提供更加安全和稳定的服务。
假设我们有两个相似的智能合约CalculatorV1.sol和CalculatorV2.sol,CalculatorV1.sol代码如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;contract CalculatorV1 {function add(uint256 a, uint256 b) public pure returns (uint256) {return a + b;}function multiply(uint256 a, uint256 b) public pure returns (uint256) {return a * b;}
}
CalculatorV2.sol代码如下:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;contract CalculatorV2 {function add(uint256 a, uint256 b) public pure returns (uint256) {return a + b;}function multiply(uint256 a, uint256 b) public pure returns (uint256) {// 优化后的乘法实现,这里简单示例,实际可能更复杂uint256 result = 0;for (uint256 i = 0; i < b; i++) {result += a;}return result;}
}
接下来编写差异化测试代码,在test目录下创建CalculatorDiffTest.t.sol:
// SPDX - License - Identifier: MIT
pragma solidity ^0.8.0;import "forge - std/Test.sol";
import "../src/CalculatorV1.sol";
import "../src/CalculatorV2.sol";contract CalculatorDiffTest is Test {CalculatorV1 calculatorV1;CalculatorV2 calculatorV2;function setUp() public {calculatorV1 = new CalculatorV1();calculatorV2 = new CalculatorV2();}function testAddConsistency() public {uint256 a = 5;uint256 b = 3;uint256 resultV1 = calculatorV1.add(a, b);uint256 resultV2 = calculatorV2.add(a, b);assertEq(resultV1, resultV2);}function testMultiplyConsistency() public {uint256 a = 5;uint256 b = 3;uint256 resultV1 = calculatorV1.multiply(a, b);uint256 resultV2 = calculatorV2.multiply(a, b);assertEq(resultV1, resultV2);}
}
在上述代码中:
- testAddConsistency和testMultiplyConsistency函数分别对两个版本合约的加法和乘法功能进行差异化测试。
- 通过调用两个版本合约的相同功能函数,并使用assertEq断言结果是否一致,来验证新版本合约的功能是否与旧版本保持一致。
运行差异化测试:forge test --match - contract CalculatorDiffTest
如果差异化测试失败,说明两个版本的合约在相同输入下产生了不同的输出,开发者需要仔细检查代码,找出差异原因并进行修复,确保新版本合约的正确性和兼容性,避免因代码更新而引入新的问题,保障智能合约的稳定运行。