Cheatcodes

大多数时候,仅仅测试您的智能合约输出是不够的。 为了操纵区块链的状态,以及测试特定的reverts和事件Events,Foundry 附带了一组Cheatcodes。

Cheatcodes允许您更改块号、您的身份等。 它们是通过在特别指定的地址上调用特定函数来调用的:0x7109709ECfa91a80626fF3989D68f67F5b1DD12D

您可以通过 Forge 标准库的“测试”合约中提供的“vm”实例轻松访问Cheatcodes。 Forge 标准库在以下 section 中有更详细的解释。

让我们为只能由其所有者调用的智能合约编写一个测试。

pragma solidity 0.8.10;

import "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 testIncrementAsOwner() public {
        assertEq(upOnly.count(), 0);
        upOnly.increment();
        assertEq(upOnly.count(), 1);
    }
}

如果我们现在运行forge test,我们将看到测试通过,因为OwnerUpOnlyTestOwnerUpOnly 的所有者。

$ forge test
Compiling 7 files with 0.8.10
Solc 0.8.10 finished in 4.25s
Compiler run successful

Running 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testIncrementAsOwner() (gas: 29162)
Test result: ok. 1 passed; 0 failed; finished in 928.64µs

让我们确保绝对不是所有者的人不能增加计数:

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

        // ...

    function testFailIncrementAsNotOwner() public {
        vm.prank(address(0));
        upOnly.increment();
    }
}

如果我们现在运行“forge test”,我们将看到所有测试都通过了。

$ forge test
No files changed, compilation skipped

Running 2 tests for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFailIncrementAsNotOwner() (gas: 8413)
[PASS] testIncrementAsOwner() (gas: 29162)
Test result: ok. 2 passed; 0 failed; finished in 1.03ms

测试通过是因为 prank Cheatcodes将我们的身份更改为下一次调用的零地址 (upOnly.increment())。 由于我们使用了 testFail 前缀,测试用例通过了,但是,使用 testFail 被认为是一种反模式(anti-pattern),因为它没有告诉我们任何关于为什么 upOnly.increment() 被revert的信息。

如果我们在启用跟踪的情况下再次运行测试,我们可以看到我们revert了正确的错误消息。

$ forge test -vvvv --match-test testFailIncrementAsNotOwner
No files changed, compilation skipped

Running 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testFailIncrementAsNotOwner() (gas: 8413)
Traces:
  [8413] OwnerUpOnlyTest::testFailIncrementAsNotOwner() 
    ├─ [0] VM::prank(0x0000000000000000000000000000000000000000) 
    │   └─ ← ()
    ├─ [247] 0xce71…c246::increment() 
    │   └─ ← 0x82b42900
    └─ ← 0x82b42900

Test result: ok. 1 passed; 0 failed; finished in 2.01ms

为了将来确定,让我们确保我们revert了,因为我们不是使用 expectRevert Cheatcodes的所有者:

contract OwnerUpOnlyTest is Test {
    OwnerUpOnly upOnly;

     // ...

    // Notice that we replaced `testFail` with `test`
    function testIncrementAsNotOwner() public {
        vm.expectRevert(Unauthorized.selector);
        vm.prank(address(0));
        upOnly.increment();
    }
}

如果我们最后一次运行 forge test,我们会看到测试仍然通过,但这次我们确信如果我们因为任何其他原因revert它总是会失败。

$ forge test
No files changed, compilation skipped

Running 2 tests for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest
[PASS] testIncrementAsNotOwner() (gas: 8739)
[PASS] testIncrementAsOwner() (gas: 29162)
Test result: ok. 2 passed; 0 failed; finished in 1.15ms

另一个可能不那么直观的Cheatcodes是 expectEmit 函数。 在查看 expectEmit 之前,我们需要了解什么是事件Events。

事件Events是合约的可继承成员。 当您发出事件Events时,参数存储在区块链上。 indexed 属性可以添加到事件Events的最多三个参数中,以形成称为 Transfer 的数据结构。 Topics允许用户搜索区块链上的事件Events。

pragma solidity 0.8.10;

import "forge-std/Test.sol";

contract EmitContractTest is Test {
    event Transfer(address indexed from, address indexed to, uint256 amount);

    function testExpectEmit() 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 testExpectEmitDoNotCheckData() 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); 时,我们想要检查下一个事件Events的第一个和第二个 indexed 主题topic。

testExpectEmit() 中预期的 Transfer 事件Events意味着我们期望 fromaddress(this),而 toaddress(1337)。 这与从 emitter.t() 发出的事件Events进行比较。

换句话说,我们正在检查来自 emitter.t() 的第一个主题topic是否等于 address(this)expectEmit 中的第三个参数设置为 false,因为不需要检查 Transfer 事件中的第三个主题topic,因为只有两个。 即使我们设置为true也没关系。

expectEmit 中的第 4 个参数设置为 true,这意味着我们要检查 "non-indexed topics",也称为数据。

例如,我们希望来自 testExpectEmit 中预期事件Events的数据(即 amount)等于实际发出的事件Events中的数据。 换句话说,我们断言 emitter.t() 发出的 amount 等于 1337。 如果 expectEmit 中的第四个参数设置为 false,我们将不会检查 amount

换句话说,testExpectEmitDoNotCheckData 是一个有效的测试用例,即使数量不同,因为我们不检查数据。


📚 参考

请参阅 Cheatcodes Reference 以获得所有可用Cheatcodes的完整概述。