Foundry Test 调试
在 Foundry 中进行测试时,如果不指定链,Foundry 会自动启动一个 anvil 链。
每次进行Foundry 测试时,都会首先调用 setUp
函数,并且每次调试都不会保留上次调试的数据。,这就涉及到为什么使用prank
将makeAddr
出来的账户设置为msg.sender
(原因之一),以及将vm.deal(user, balance)
放在 setUp
函数里面。
forge test 参数
1 | forge test # 进行所有的测试 |
forge snapshot
forge snapshot 的作用:创建每个测试的 Gas 使用快照
简单使用方法:
1 | forge snapshot |
以下描述来自 Foundry 官方文档:
创建每个测试的 Gas 使用快照。
结果被写入一个名为
.gas-snapshot
的文件中。你可以通过传递--snap <PATH>
来改变该文件的名称。在快照中默认包括模糊测试。它们使用一个静态种子来实现确定性的结果。
快照可以用
--diff
和--check
来比较。第一个标志将输出一个差异,第二个标志将输出一个差异 同时 如果快照不匹配,则以代码 1 退出。
选项&参数
1 | # 选项 |
其他参数:https://learnblockchain.cn/docs/foundry/i18n/zh/reference/forge/forge-snapshot.html
forge coverage
forge coverage 的作用:计算当前合约测试的覆盖率并输出。
选项&参数
1 | --report允许您指定要使用的报告类型覆盖范围。该标志可以多次使用。 |
可以联合重定向符号,将覆盖范围数据存储到 txt 文件中。
1 | forge --report debug > coverage.txt |
基本测试
在 Foundry 框架中,有一些内置库和工具可以十分方便的测试合约,所以在编写一个测试合约的时候大致框架如下:
1 | pragma solidity ^0.8.0; |
简单的测试(从 setUp
开始)
在所有的测试中,第一件都是设置 setUp() 函数,在这里部署合约,然后就可以写需要进行的测试,举例:
1 | pragma solidity ^0.8.0; |
测试结果:
console.log 测试、调试
另一种测试和调试的方法是使用console.log
,Test是一个很大的库,还有另一个库叫做 console
:
1 | // SPDX-License-Identifier: MIT |
Arrange、Act、Assert 测试方法
首先安排测试环境(Arrange),然后考虑要测试的操作(Act),最后进行测试断言(Assert)。
举例:(对 FundMe 合约的 withDrawWithSingleFunder函数 进行调试)
这里的 funded 就是这里举例的 modifier
1 | function testWithDrawWithSignleFunder() public funded { |
模糊测试(fuzz)
测试是否按照预期回滚
测试交易是否会按照预期回滚会使用vm.expectRevert()
这个作弊码来实现。具体关于这个作弊码查看Foundry作弊码部分。
这里涉及到自定义错误时,可以使用下面的方法:
1 | function testCantEnterWhenRaffleIsCalculating() public { |
如果自定义错误还有额外的参数,就需要联合使用abi
编码中的abi.encodeWithSelector
:
1 | function testPerformUpKeeperRevertsIfcheckUpkeepIsFalse() public { |
对于调试时需要多个地址的解决方法:
在使用 Foundry 进行调试时,有些时候会出现需要模拟多个地址来和合约进行交互的场景,这个时候就需要善用一些 Solidity 的特性了。
可以通过利用 for
循环,加上address()
将 uint160
类型的整数转换为地址的方式来实现该过程
举例:
1 | function testWithdrawFromMultipleFunders() public funded { |
注意:
- 从 Solidity 0.8.0 版本开始,使用
address()
进行类型转换时,有了更严格的要求,所以要使用uint160
的形式定义上面的数据 for
循环开始遍历时,不能将0
作为开始,因为0x0
地址在进行使用时,有些时候交易会发生回滚revert
Foundry 测试时关于 Gas 相关:
想要知道一个测试花费了多少 Gas,可以使用上面的[forge snapshot](##forge snapshot)
关于测试时花费 Gas 相关问题:
在调试过程中使用vm.prank
并执行一笔交易时,我们应该花费一定的 Gas ,这与实际链上的 Gas 价格有关,当使用 anvil(Foundry 内置的本地测试链)时,默认情况下,gas 价格会被设置为 0。所以,在 anvil 本地链开发,无论是否分叉,gas 价格实际上都默认为零。为了模拟具有实际 gas 价格的交易,我们需要告诉测试模拟使用的真实的 gas 价格,可以使用一个作弊码txGasPrice
来设置 Gas 的值
示例:
1 | function testWithDrawWithSignleFunder() public funded { |
Foundry Test 关于事件(event
)的测试
Foundry 测试事件是否正常触发
在 Foundry 中测试时间是否正常触发需要使用一个作弊码(cheat code)vm.expectEmit
。该作弊码有两个签名,实际上就是传入四个(或五个)这些参数对应:
- topic1、topic2、topic3:该事件是否有主题,有几个(有一个,topic1 为
true
,其他为false
;有两个,topic1、topic2 为true
,其他为false
以此类推) - Log:时间有没有非
indexed
的参数,有为true
,没有为false
- address:可选参数,如果需要判断事件的触发者,这传递要断言的事件触发者,如果不需要,则忽略该参数。
在 Foundry 中写 Test 脚本的的时候,需要引入要触发的事件(事件通过 import 无法导入,需要按照原来要定义的事件进行重新定义)。
事件的顺序很重要:
如果我们调用
expectEmit
并触发了一个事件,那么下一个被触发的事件必须与我们的预期一致
示例:
没有地址的签名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14event Transfer(address indexed from, address indexed to, uint256 ammount);
function testERC20EmitsTransfer() public {
// 只有`from`和`to`在ERC20的“Transfer”事件中是 inded,
// 所以我们专门检查topics 1和2(默认情况下总是检查topic 0),
// 以及数据(`amount`)。
vm.expectEmit(true, true, false, true);
// 我们出发的事件:我们预期触发的的事件:
emit MyToken.Transfer(address(this), address(1), 10);
// 执行调用
myToken.transfer(address(1). 10);
}有签名的地址:
1
2
3
4
5
6
7
8
9event Transfer(address indexed from, address indexed to, uint256 amount);
function testERC20EmitsTransfer() public {
// 通过将地址作为第五个参数传递来检测时间的触发者是否为预期的触发者
vm.expectEmit(true, true, false, true, address(myToken));
emit myToken.Transfer(address(this), address(1), 10);
myToken.transfer(address(1), 10);
}断言在单次调用中触发多个事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14function testERC20EmitsBatchTransfer() public {
// 声明多个预期的转账事件
for (uint256 i = 0; i< users.length; i++) {
vm.expectEmit(true, true, true, true);
emit Transfer(address(this), users[i], 10);
}
// 我们还预期一个自定义的 `BatchTransfer(uint256 numverOfTransfers)` event。
vm.expectEmit(false, false, false, false);
emit BatchTransfer(users.length);
// 执行调用
myToken.batchTransfer(users, 10);
}
测试事件是否能正确触发
测试时间是否能正确触发,需要用到两个作弊码:recordLogs
和getRecordedLogs
。
vm.recordLogs
:记录接下来触发的事件vm.getRecordedLogs
:读取由recordLogs
记录的事件。
再使用这两个作弊码来判断事件是否为预期的参数触发,需要按照内置的格式:
1 | struct Log { |
而要判断是否为预期触发,可以参考下面的例子:(主义要导入Vm 合约)
1 |
分叉测试相关若干问题
使用了外部组件需要传入私钥(eg. 绑定 Chainlink 中间件)
分叉测试时,涉及到了需要传入私钥,但是私钥在合约中硬编码会产生安全问题(Anvil 本地链可以硬编码)所以,这个时候,可以使用作弊码vm.envUint
来从环境变量中读取私钥,配合vm.startBroadcast()
使得下一次的交易发起者是该私钥对应的用户。
Foundry 调试更佳实践
使用状态树(state tree)来组织单元测试
使用状态树(state tree)来组织单元测试,找到具有驱动合约行为的父节点特定状态条件。
如果是我们自己的合约,在写一些测试来测试某些东西,**一旦测试通过,就为其创建一个 modifier
**,这样就不必再测试中复制粘贴代码
比如:大部分的调试都需要首先进行使用 vm.prank(USER)
来设置msg.sender
并且需要调用特定的函数来存款(如:fundMe.fund{value: SEND_VALUE}()
)那么,就可以通过创建一个 modifier
来解决:
1 | modifier funded() { |
Foundry 内置的小的 Solidity 编译器——chisel
在安装了 Foundry 之后,可以使用 chisel
来对一些简单的 Solidity 代码进行测试,只需在终端输入 chisel
即可,它允许在其自己的终端中输入一小段 Solidity 代码并且运行。
–未完待续–