导购网站免费推广,知名商业网站有哪些,千助做网站怎么样,wordpress 文章颜色安装 Foundry
如果你还没有安装 Foundry#xff0c;请按照此处的说明进行操作#xff1a;Foundry 安装
Foundry Hello World
只需运行以下命令#xff0c;它将为你设置环境#xff0c;创建测试并运行它们。#xff08;当然#xff0c;这假设你已经安装了 Foundry…安装 Foundry
如果你还没有安装 Foundry请按照此处的说明进行操作Foundry 安装
Foundry Hello World
只需运行以下命令它将为你设置环境创建测试并运行它们。当然这假设你已经安装了 Foundry。
forge init
forge testSolidity 测试最佳实践
无论使用何种框架solidity 单元测试的质量取决于三个因素
行覆盖率分支覆盖率以及完全定义的状态转换。
通过理解每一个因素我们可以说明为何我们专注于 Foundry API 的某些方面。
当然不可能为所有可能的输入输出范围编写文档。然而测试质量通常与行覆盖率、分支覆盖率和定义的状态转换相关。在我们的另一篇文章中我们已经记录了如何使用 Foundry 测量行和分支覆盖率 。我们将在此解释这三个度量指标的重要性
1. 行覆盖率
行覆盖率就是它字面上的意思。如果代码行在测试中未被执行则行覆盖率不是 100%。如果代码行从未被执行过你无法确定它是否按预期工作或会抛出异常。在智能合约中没有不实现 100%行覆盖率的理由。如果你在编写代码这意味着你期望它在未来的某个时候被执行那么为什么不对其进行测试呢
2. 分支覆盖率
即使每一行都被执行了也不意味着每一种智能合约业务逻辑的变化都被测试了。
考虑以下函数
function changeOwner(address newOwner) external {require(msg.sender owner, onlyOwner);owner newOwner;
}如果通过调用此地址的所有者来测试它你将获得 100%的行覆盖率但不会获得 100%的分支覆盖率。这是因为 require 语句和所有者分配都被执行了但 require 抛出异常的情况没有被测试。
这里是一个更微妙的例子。
// notice anyone can pay off someone elses loan
// param debtor the person whos loan the sender is making a payment for
function payDownLoan(address debtor) external payable {uint256 loanAmount loanAmounts[debtor];require(loanAmount 0, no such loan);if (msg.value debtAmount) {loanAmounts[debtor] 0;emit LoanFullyRepaid(debtor);} else {emit LoanPayment(debtor, debtAmount, msg.value);loanAmount - msg.value;}if (msg.value loanAmount) {msg.sender.call{value: msg.value - loanAmount}();}
}在这种情况下有多少个分支需要测试
贷款为零的情况某人支付少于贷款金额的情况某人支付正好等于贷款金额的情况某人支付超过贷款金额的情况
通过发送多于或少于贷款金额的以太币可以在此测试中获得 100%的行覆盖率。这将执行 if else 语句的两个分支以及最后的 if 语句但这不会测试贷款正好付清为零的 else 语句。
你的函数分支越多单元测试它们就越困难。技术术语为圈复杂度 。
3. 完全定义的状态转换
高质量的 solidity 单元测试尽可能详细地记录状态转换。状态转换包括
存储变量的改变合约的部署或自毁以太余额的变化事件的触发带有某些消息交易的回退带有某些错误消息
如果函数执行了这些动作修改状态的确切方式应该在单元测试中被捕获任何偏差都应导致回退。这样任何意外的修改无论多么微小都会自动被捕捉。回到之前的例子应测试哪些状态转换
合约中的 Ether 增加了借款人偿还贷款的等量跟踪贷款金额的存储变量按预期金额减少当发送者为不存在的贷款付款时出现预期的错误消息触发相应的事件和相关消息
如果智能合约的业务逻辑发生变化测试应失败。在其他领域这通常被认为是一个“脆弱”的单元测试。它可能会减慢源代码的迭代速度。但 Solidity 代码通常是一次编写且永不更改因此这对智能合约测试来说不是问题。
4. 单元测试最佳实践结论
在记录 Foundry 单元测试工作原理之前为什么我们要覆盖所有这些因为这可以帮助我们隔离大多数情况下会使用的高影响测试工具。Foundry 的功能非常广泛但多数测试用例中只会用到一小部分。
Foundry 断言
为了确保状态转换确实发生你将需要断言。让我们从你调用forge init后 Foundry 提供的默认测试文件开始。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;import forge-std/Test.sol;
import ../src/Counter.sol;contract CounterTest is Test {Counter public counter;function setUp() public {counter new Counter();counter.setNumber(0);}function testIncrement() public {counter.increment();assertEq(counter.number(), 1);}function testSetNumber(uint256 x) public {counter.setNumber(x);assertEq(counter.number(), x);}
}setUp() 函数部署你正在测试的合约以及生态系统中的其他合约。
任何以test开头的函数将被执行为单元测试。不以test开头的函数将不会被执行除非它们被test或setUp函数调用。
这里 可以找到你可以使用的断言。
你最常用的有
assertEq断言相等assertLt断言小于assertLe断言小于或等于assertGt断言大于assertGe断言大于或等于assertTrue断言为真
前两个传递给 assert 的参数是比较内容但你也可以添加一个作为第三个参数的帮助错误信息你应该总是这样做尽管默认示例没有显示。以下是推荐的写断言的方式
function testIncrement() public {counter.increment();assertEq(counter.number(), 1, expect x to equal to 1);
}function testSetNumber(uint256 x) public {counter.setNumber(x);assertEq(counter.number(), x, x should be setNumber);
}使用 Foundry vm.prank 修改 msg.sender
Foundry 更有趣的方法来改变发送者账户或钱包是 vm.prank APIFoundry 称之为作弊码。
这是一个最小的示例
function testChangeOwner() public {vm.prank(owner);contractToTest.changeOwner(newOwner);assertEq(contractToTest.owner(), newOwner);
}vm.prank 仅对紧随其后的事务有效。如果你想使用同一个地址进行一系列交易请使用 vm.startPrank 并在结束后使用 vm.stopPrank。
function testMultipleTransactions() public {vm.startPrank(owner);// 表现为所有者vm.stopPrank();
}在 Foundry 中定义账户和地址
上面的 owner 变量可以用几种方式定义
// 将十进制转换为地址创建的地址
address owner address(1234);// vitalik 的地址
address owner 0x0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;// 从已知私钥创建一个地址;
address owner vm.addr(privateKey);// 创建一个攻击者
address hacker 0x00baddad;msg.sender 和 tx.origin 的恶作剧
在上述示例中msg.sender 被更改。如果你想同时控制 tx.origin 和 msg.sendervm.prank 和 vm.startPrank 都可以选择性地接受两个参数其中第二个参数是 tx.origin。
vm.prank(msgSender, txOrigin);依赖于 tx.origin 通常是一个坏习惯所以你很少需要使用带两个参数版本的 vm.prank。
检查余额
当你转移以太币时你应该测量余额是否按预期变化。值得庆幸的是在 Foundry 中检查余额很容易因为它是用 Solidity 编写的。
考虑这个合约
contract Deposit {event Deposited(address indexed);function buyerDeposit() external payable {require(msg.value 1 ether, incorrect amount);emit Deposited(msg.sender);}// 逻辑的其他部分
}测试函数如下所示。
function testBuyerDeposit() public {uint256 balanceBefore address(depositContract).balance;depositContract.buyerDeposit{value: 1 ether}();uint256 balanceAfter address(depositContract).balance;assertEq(balanceAfter - balanceBefore, 1 ether, expect increase of 1 ether);
}请注意我们没有测试买家发送的金额不是 1 以太币的情况这会导致回滚。我们将在下一节讨论测试回滚。
使用 vm.expectRevert 预计回滚
当前形式的上述测试的问题在于你可以删除 require 语句测试仍然会通过。让我们改进测试以使删除 require 语句会导致测试失败。
function testBuyerDepositWrongPrice() public {vm.expectRevert(incorrect amount);depositContract.deposit{value: 1 ether 1 wei}();vm.expectRevert(incorrect amount);depositContract.deposit{value: 1 ether - 1 wei}();
}请注意必须在我们预计要回滚的函数调用之前立即调用 vm.expectRevert。现在如果我们删除 require 语句它将回滚因此我们更好地模拟了智能合约的预期功能。
测试自定义错误
如果我们使用自定义错误而不是 require 语句测试回滚的方法如下
contract CustomErrorContract {error SomeError(uint256);function revertError(uint256 x) public pure {revert SomeError(x);}
}测试文件如下所示
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;import forge-std/Test.sol;
import ../src/RevertCustomError.sol;contract CounterTest is Test {CustomErrorContract public customErrorContract;error SomeError(uint256);function setUp() public {customErrorContract new CustomErrorContract();}function testRevert() public {// 5 是一个任意示例vm.expectRevert(abi.encodeWithSelector(SomeError.selector, 5));customErrorContract.revertError(5);}
}在我们的示例中我们创建了一个参数化的自定义错误。为了使测试通过参数需要等于在回滚期间实际使用的参数。
使用 vm.expectEvent 测试日志和事件
虽然 solidity 事件 不会改变智能合约的功能但错误地实现它们会破坏读取智能合约状态的客户端应用程序。为了确保我们的事件按预期工作我们可以使用 vm.expectEmit。这个 API 的行为相当反直觉因为你必须在测试中发出事件以确保它在智能合约中工作。
这是一个最小的示例。
function testBuyerDepositEvent() public {vm.expectEmit();emit Deposited(buyer);depositContract.deposit{value: 1 ether}();
}使用 vm.warp 调整 block.timestamp
现在让我们考虑一个时间锁定的提现。卖方可以在 3 天后提现付款。
contract Deposit {address public seller;mapping(address uint256) public depositTime;event Deposited(address indexed);event SellerWithdraw(address indexed, uint256 indexed);constructor(address _seller) {seller _seller;}function buyerDeposit() external payable {require(msg.value 1 ether, incorrect amount);uint256 _depositTime depositTime[msg.sender];require(_depositTime 0, already deposited);depositTime[msg.sender] block.timestamp;emit Deposited(msg.sender);}function sellerWithdraw(address buyer) external {require(msg.sender seller, not the seller);uint256 _depositTime depositTime[buyer];require(_depositTime ! 0, buyer did not deposit);require(block.timestamp - _depositTime 3 days, refund period not passed);delete depositTime[buyer];emit SellerWithdraw(buyer, block.timestamp);(bool ok, ) msg.sender.call{value: 1 ether}();require(ok, seller did not withdraw);}
}我们添加了许多需要测试的功能但现在让我们重点放在时间方面。
我们想测试卖方在存款后的 3 天内不能提取资金。显然缺少一个买方在该时间窗口前提取的函数但我们稍后会讨论。
请注意block.timestamp 默认从 1 开始。这不是一个实际的测试数字因此我们应该首先转换到当前日期。
可以使用 vm.warp(x) 来实现这个功能但我们可以更讲究地使用修饰符。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;import forge-std/Test.sol;
import ../src/Deposit.sol;contract DepositTest is Test {Deposit public deposit;Deposit public faildeposit;address constant SELLER address(0x5E11E7);//address constant Rejector address(RejectTransaction);RejectTransaction private rejector;event Deposited(address indexed);event SellerWithdraw(address indexed, uint256 indexed);function setUp() public {deposit new Deposit(SELLER);rejector new RejectTransaction();faildeposit new Deposit(address(rejector));}modifier startAtPresentDay() {vm.warp(1680616584);_;}address public buyer address(this); // DepositTest 合约即为“买家”address public buyer2 address(0x5E11E1); // 随机地址address public FakeSELLER address(0x5E1222); // 随机地址function testDepositAmount() public startAtPresentDay {// 此测试检查买家只能存入 1 ethervm.startPrank(buyer);vm.expectRevert();deposit.buyerDeposit{value: 1.5 ether}();vm.expectRevert();deposit.buyerDeposit{value: 2.5 ether}();vm.stopPrank();}
}使用 vm.roll 调整 block.number
如果你想在 Foundry 中调整区块号 (block.number)使用
vm.roll(blockNumber)来改变区块号。要向前移动一定数量的区块请执行以下操作
vm.roll(block.number() numberOfBlocks)添加额外的测试
为了完整性让我们为其余的功能编写单元测试。一些额外的功能需要为存款功能进行测试
公共变量 depositTime 与交易时间匹配用户不能重复存款
以及卖家功能的测试
卖家不能为不存在的地址提款买家的条目被删除这允许买家重新购买触发 SellerWithdraw 事件合约的余额减少 1 ether不是卖家的地址调用 sellerWithdraw 会被回滚
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;import forge-std/Test.sol;
import ../src/Deposit.sol;contract DepositTest is Test {Deposit public deposit;Deposit public faildeposit;address constant SELLER address(0x5E11E7);//address constant Rejector address(RejectTransaction);RejectTransaction private rejector;event Deposited(address indexed);event SellerWithdraw(address indexed, uint256 indexed);function setUp() public {deposit new Deposit(SELLER);rejector new RejectTransaction();faildeposit new Deposit(address(rejector));}modifier startAtPresentDay() {vm.warp(1680616584);_;}address public buyer address(this); // DepositTest 合约即为“买家”address public buyer2 address(0x5E11E1); // 随机地址address public FakeSELLER address(0x5E1222); // 随机地址function testDepositAmount() public startAtPresentDay {// 此测试检查买家只能存入 1 ethervm.startPrank(buyer);vm.expectRevert();deposit.buyerDeposit{value: 1.5 ether}();vm.expectRevert();deposit.buyerDeposit{value: 2.5 ether}();vm.stopPrank();}function testBuyerDepositSellerWithdrawAfter3days() public startAtPresentDay {// 此测试检查买家存款后 3 天卖家能够提款// 买家存款 1 ethervm.startPrank(buyer); // msg.sender buyerdeposit.buyerDeposit{value: 1 ether}();assertEq(address(deposit).balance, 1 ether, Contract balance did not increase); // 检查合约的余额是否增加vm.stopPrank();// 三天后卖家提款vm.startPrank(SELLER); // msg.sender SELLERvm.warp(1680616584 3 days 1 seconds);deposit.sellerWithdraw(address(this));assertEq(address(deposit).balance, 0 ether, Contract balance did not decrease); // 检查合约的余额是否减少}function testBuyerDepositSellerWithdrawBefore3days() public startAtPresentDay {// 此测试检查买家存款后 3 天卖家能够提款// 买家存款 1 ethervm.startPrank(buyer); // msg.sender buyerdeposit.buyerDeposit{value: 1 ether}();assertEq(address(deposit).balance, 1 ether, Contract balance did not increase); // 检查合约的余额是否增加vm.stopPrank();// 三天前卖家提款vm.startPrank(SELLER); // msg.sender SELLERvm.warp(1680616584 2 days);vm.expectRevert(); // 预期会回滚deposit.sellerWithdraw(address(this));}function testdepositTimeMatchesTimeofTransaction() public startAtPresentDay {// 此测试检查公共变量 depositTime 是否与交易时间匹配vm.startPrank(buyer); // msg.sender buyerdeposit.buyerDeposit{value: 1 ether}();// 检查它是否存入于正确的时间assertEq(deposit.depositTime(buyer),1680616584, // startAtPresentDay 的时间Time of Deposit Doesnt Match);vm.stopPrank();}function testUserDepositTwice() public startAtPresentDay {// 此测试检查用户不能重复存款 vm.startPrank(buyer); // msg.sender buyerdeposit.buyerDeposit{value: 1 ether}();vm.warp(1680616584 1 days); // 一天后...vm.expectRevert();deposit.buyerDeposit{value: 1 ether}(); // 应该回滚因为未到 3 天}function testNonExistantContract() public startAtPresentDay {// 此测试检查卖家不能为不存在的地址提款vm.startPrank(SELLER); // msg.sender SELLERvm.expectRevert();deposit.sellerWithdraw(buyer); }function testBuyerBuysAgain() public startAtPresentDay {// 此测试检查买家的条目是否被删除这允许买家重新购买vm.startPrank(buyer); // msg.sender buyerdeposit.buyerDeposit{value: 1 ether}();vm.stopPrank();// 卖家提款vm.warp(1680616584 3 days 1 seconds);vm.startPrank(SELLER); // msg.sender SELLERdeposit.sellerWithdraw(buyer);vm.stopPrank();// 检查 depositTime[buyer] 0
assertEq(deposit.depositTime(buyer), 0, 买家的条目未被删除);// 买家再次存款
vm.startPrank(buyer); // msg.sender buyer
vm.expectEmit();
emit Deposited(buyer);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
}function testSellerWithdrawEmitted() public startAtPresentDay {
// 此测试检查 SellerWithdraw 事件是否被触发// buyer2 存款
vm.deal(buyer2, 1 ether); // msg.sender buyer2
vm.startPrank(buyer2);
vm.expectEmit(); // 检查 Deposited 事件
emit Deposited(buyer2);
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();vm.warp(1680616584 3 days 1 seconds); // 3 天 1 秒后...// 卖家提款 检查 SellerWithdraw 事件是否被触发
vm.startPrank(SELLER); // msg.sender SELLER
vm.expectEmit(); // 期望 SellerWithdraw 事件被触发
emit SellerWithdraw(buyer2, block.timestamp);
deposit.sellerWithdraw(buyer2);
vm.stopPrank();
}function testFakeSeller2Withdraw() public startAtPresentDay {
// 买家存款
vm.startPrank(buyer);
vm.deal(buyer, 2 ether); // 该合约地址是买家
deposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(deposit).balance, 1 ether, 以太存款失败);vm.warp(1680616584 3 days 1 seconds); // 3 天 1 秒后...vm.startPrank(FakeSELLER); // msg.sender FakeSELLER
vm.expectRevert();
deposit.sellerWithdraw(buyer);
vm.stopPrank();
}function testRejectedWithdrawl() public startAtPresentDay {
// 此测试检查买家的条目是否被删除这允许买家再次购买vm.startPrank(buyer); // msg.sender buyer
faildeposit.buyerDeposit{value: 1 ether}();
vm.stopPrank();
assertEq(address(faildeposit).balance, 1 ether, 断言失败);vm.warp(1680616584 3 days 1 seconds); // 3 天 1 秒后...vm.startPrank(address(rejector)); // msg.sender rejector
vm.expectRevert();
faildeposit.sellerWithdraw(buyer);
vm.stopPrank();
}测试失败的以太转账
测试买家提款需要额外的技巧来获得完整的行覆盖率。以下是我们正在测试的代码片段我们将在上面的代码中解释 Rejector 合约。
function buyerWithdraw() external {uint256 _depositTime depositTime[msg.sender];require(_depositTime ! 0, sender did not deposit);require(block.timestamp - _depositTime 3 days);emit BuyerRefunded(msg.sender, block.timestamp);// 这是我们正在测试的分支(bool ok,) msg.sender.call{value: 1 ether}();require(ok, Failed to withdraw);
}为了测试 require(ok…) 的失败条件我们需要让以太转账失败。测试通过创建一个调用 buyerWithdraw 函数的智能合约来实现这一点但其 receive 函数设置为 revert。
Foundry 模糊测试
虽然我们可以指定一个不是卖家的任意地址来测试未授权地址提款的 revert但尝试许多不同的值更令人放心。
如果我们为测试函数提供参数Foundry 将尝试许多不同的参数值。为了防止它使用不适用于测试用例的参数例如当地址被授权时我们将使用 vm.assume。以下是如何测试未授权卖家的卖家提款。
// notSeller 将被随机选择
function testInvalidSellerAddress(address notSeller) public {vm.assume(notSeller ! seller);vm.expectRevert(not the seller);depositContract.sellerWithdraw(notSeller);
}以下是所有的状态转换
合约的 balance 减少了 1 etherBuyerRefunded 事件被触发买家可以在三天内退款
以下是需要测试的分支
买家不能在 3 天后提款买家如果从未存款则不能提款
Console.log Foundry
要在 Foundry 中使用 console.log请导入以下内容
import forge-std/console.sol;并使用以下命令运行测试
forge test -vv测试签名
请参阅我们关于 solidity 签名验证 的教程因此我们建议你参考该教程。
Solidity 测试内部函数 请参阅我们关于 solidity 测试内部函数 的教程。
使用 vm.deal 和 vm.hoax 设置地址余额
作弊码 vm.hoax 允许你同时恶作剧一个地址并设置其余额。
vm.hoax(addressToPrank, balanceToGive);
// 下一个调用是 addressToPrank 的恶作剧vm.deal(alice, balanceToGive);Foundry 的一些常见错误
在接收以太时没有回退函数
如果你正在测试从合约中提取以太它将被发送到运行测试的合约。Foundry 测试本身是一个智能合约如果你将以太发送到没有 fallback 或 receive 函数的智能合约则交易将失败。确保在合约中有一个 fallback 或 receive 函数。
在接收代币时没有 onERC…Received
同样ERC-721 safeTransferFrom 和 ERC-1155 transferFrom 在将代币发送到没有适当传输钩子函数的智能合约时会回滚。如果你想测试将 NFT或类似 ERC777 的代币转移给自己你需要将其添加到测试中。
总结
目标是 100% 的行和分支覆盖率完全定义预期的状态转换在断言中使用错误消息
了解更多
要了解超越单元测试和基本模糊测试的高级 solidity 测试https://t.me/gtokentool。