作弊码(Cheatcodes)
大多数时候,仅仅测试你的智能合约输出是不够的。 为了操纵区块链的状态,以及测试特定的 reverts 和事件 Events,Foundry 附带了一组作弊码(Cheatcodes)。
作弊码允许你更改区块号、你的身份(地址)等。 它们是通过在特别指定的地址上 (0x7109709ECfa91a80626fF3989D68f67F5b1DD12D
)调用特定函数来调用的。
你可以通过 Forge 标准库的 Test
合约中提供的 vm
实例轻松访问作弊码。 Forge 标准库在以下 部分 中有更详细的解释。
让我们为验证只能由所有者调用的智能合约编写一个测试。
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
error Unauthorized();
contract OwnerUpOnly {
address public immutable owner;
uint256 public count;
constructor() {
owner = msg.sender;
}
function increment() external {
if (msg.sender != owner) {
revert Unauthorized();
}
count++;
}
}
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
function setUp() public {
upOnly = new OwnerUpOnly();
}
function test_IncrementAsOwner() public {
assertEq(upOnly.count(), 0);
upOnly.increment();
assertEq(upOnly.count(), 1);
}
}
如果我们现在运行 forge test
,我们将看到测试通过,因为 OwnerUpOnlyTest
是 OwnerUpOnly
的所有者(owner)。
$ forge test
Compiling 24 files with Solc 0.8.10
Solc 0.8.10 finished in 1.12s
Compiler run successful!
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 29161)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 420.34µs (54.08µs CPU time)
Ran 1 test suite in 5.73ms (420.34µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
让我们确保绝对不是所有者的人不能增加计数:
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
// ...
function testFail_IncrementAsNotOwner() public {
vm.prank(address(0));
upOnly.increment();
}
}
如果我们现在运行 forge test
,我们将看到所有测试都通过了。
$ forge test
No files changed, compilation skipped
Ran 2 tests for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 8314)
[PASS] test_IncrementAsOwner() (gas: 29161)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 431.98µs (144.86µs CPU time)
Ran 1 test suite in 5.78ms (431.98µs CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
测试通过是因为 prank
作弊码将我们的身份更改为零地址再进行下一次调用 (upOnly.increment()
)。 由于我们使用了 testFail
前缀,测试用例通过了,但是,使用 testFail
被认为是一种反模式(anti-pattern),因为它没有告诉我们任何关于为什么 upOnly.increment()
被 revert 的信息。
如果我们在启用跟踪的情况下再次运行测试,我们可以看到 revert 了正确的错误消息。
$ forge test -vvvv --match-test testFail_IncrementAsNotOwner
No files changed, compilation skipped
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFail_IncrementAsNotOwner() (gas: 8314)
Traces:
[8314] OwnerUpOnlyTest::testFail_IncrementAsNotOwner()
├─ [0] VM::prank(0x0000000000000000000000000000000000000000)
│ └─ ← [Return]
├─ [247] OwnerUpOnly::increment()
│ └─ ← [Revert] Unauthorized()
└─ ← [Revert] Unauthorized()
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 402.26µs (57.90µs CPU time)
Ran 1 test suite in 5.84ms (402.26µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
为了将来的确定性,让我们确保我们 revert 了,可以使用expectRevert
作弊码来验证我们不是所有者的情况:
contract OwnerUpOnlyTest is Test {
OwnerUpOnly upOnly;
// ...
// Notice that we replaced `testFail` with `test`
function test_RevertWhen_CallerIsNotOwner() public {
vm.expectRevert(Unauthorized.selector);
vm.prank(address(0));
upOnly.increment();
}
}
如果我们最后再一次运行 forge test
,我们会看到测试仍然通过,但这次我们确信如果因为任何其他原因 revert 时,它将总是会失败。
$ forge test
No files changed, compilation skipped
Ran 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] test_IncrementAsOwner() (gas: 29161)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 352.09µs (47.11µs CPU time)
Ran 1 test suite in 5.86ms (352.09µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
另一个可能不那么直观的作弊码是 expectEmit
函数。 在查看 expectEmit
之前,我们需要了解什么是事件(Event)。
事件是合约的可继承成员。 当触发出事件时,参数存储在区块链上。 indexed
属性最多可以添加到事件三个参数中,以形成称为 “主题(topic)” 的数据结构。 主题允许用户搜索(过滤)区块链上的事件。
pragma solidity 0.8.10;
import {Test} from "forge-std/Test.sol";
contract EmitContractTest is Test {
event Transfer(address indexed from, address indexed to, uint256 amount);
function test_ExpectEmit() public {
ExpectEmit emitter = new ExpectEmit();
// Check that topic 1, topic 2, and data are the same as the following emitted event.
// Checking topic 3 here doesn't matter, because `Transfer` only has 2 indexed topics.
vm.expectEmit(true, true, false, true);
// The event we expect
emit Transfer(address(this), address(1337), 1337);
// The event we get
emitter.t();
}
function test_ExpectEmit_DoNotCheckData() public {
ExpectEmit emitter = new ExpectEmit();
// Check topic 1 and topic 2, but do not check data
vm.expectEmit(true, true, false, false);
// The event we expect
emit Transfer(address(this), address(1337), 1338);
// The event we get
emitter.t();
}
}
contract ExpectEmit {
event Transfer(address indexed from, address indexed to, uint256 amount);
function t() public {
emit Transfer(msg.sender, address(1337), 1337);
}
}
当我们调用 vm.expectEmit(true, true, false, true);
时,我们想要检查下一个事件的第一个和第二个 indexed
主题。
test_ExpectEmit()
中预期的 Transfer
事件意味着我们期望 from
是 address(this)
,而 to
是 address(1337)
。 这与从 emitter.t() 发出的事件进行比较。
换句话说,我们正在检查来自 emitter.t()
的第一个主题是否等于 address(this)
。 expectEmit
中的第三个参数设置为 false
,因为不需要检查 Transfer
事件中的第三个主题,因为只有两个主题。 即使我们设置为 true
也没关系。
expectEmit
中的第 4 个参数设置为 true
,这意味着我们要检查 "non-indexed topics(非索引主题)",也称为数据。
例如,我们希望来自 test_ExpectEmit
中预期事件的数据(即 amount
)等于实际发出的事件中的数据。 换句话说,我们断言 emitter.t()
发出的 amount
等于 1337
。 如果 expectEmit
中的第四个参数设置为 false
,我们将不会检查 amount
。
换句话说,test_ExpectEmit_DoNotCheckData
是一个有效的测试用例,即使数量不同,因为我们不检查数据。
📚 参考
请参阅 作弊码 参考 以获得所有可用作弊码的完整概述。