分叉测试

Forge 支持使用两种不同的方法在分叉环境中进行测试:

使用哪种方法? 分叉模式提供针对特定分叉环境运行整个测试套件,而分叉cheatcodes提供更大的灵活性和表现力,可以在测试中使用多个分叉。 您的特定用例和测试策略将有助于告知使用哪种方法。

分叉模式

要在分叉环境(例如分叉的以太坊主网)中运行所有测试,请通过 --fork-url 标志传递 RPC URL:

forge test --fork-url <your_rpc_url>

以下值已更改以反映分叉时链的值:

可以使用 --fork-block-number 指定要从中分叉的块:

forge test --fork-url <your_rpc_url> --fork-block-number 1

当您需要与现有合约进行交互时,分叉特别有用。 您可以选择以这种方式进行集成测试,就好像您在实际网络上一样。

缓存

如果同时指定了 --fork-url--fork-block-number,那么该块的数据将被缓存以供将来的测试运行。

数据缓存在 ~/.foundry/cache/rpc/<chain name>/<block number> 中。 要清除缓存,只需删除目录或运行 forge clean(删除所有构建工件和缓存目录)。

也可以通过传递 --no-storage-caching 或通过配置 no_storage_cachingfoundry.toml 完全忽略缓存 rpc_storage_caching

已改进的traces

Forge 支持使用 Etherscan 在分叉环境中识别合约。

要使用此功能,请通过 --etherscan-api-key 标志传递 Etherscan API 密钥:

forge test --fork-url <your_rpc_url> --etherscan-api-key <your_etherscan_api_key>

或者,您可以设置 ETHERSCAN_API_KEY 环境变量。

分叉cheatcodes

分叉cheatcodes允许您在 Solidity 测试代码中以编程方式进入分叉模式。 这些作弊码允许您在逐个测试的基础上使用分叉模式,并在测试中使用多个分叉,而不是通过 forge CLI 参数配置分叉模式。 每个叉子都通过其自己唯一的 uint256 标识符进行识别。

用法

重要的是要记住,所有_测试函数都是隔离的,这意味着每个测试函数都使用状态_after setUp 的_副本_执行,并在其自己的独立 EVM 中执行。

因此,在 setUp 期间创建的分支可用于测试。 下面的代码示例使用 createFork 创建两个分叉,但_不_一开始就选择一个。 每个 fork 都有一个唯一标识符 (uint256 forkId),该标识符在首次创建时分配。

通过将该 forkId 传递给 selectFork 来启用特定的分叉。

createSelectForkcreateFork 加上 selectFork 的单行代码。

一次只能有一个活动分叉,当前活动分叉的标识符可以通过 activeFork 检索。

类似于 roll,您可以使用 rollFork 设置分叉的 block.number

要了解选择分叉时会发生什么,了解分叉模式的一般工作方式很重要:

每个分叉都是一个独立的 EVM,即所有分叉都使用完全独立的存储。 唯一的例外是 msg.sender 的状态和测试合约本身,它们在分叉交换中是持久的。 换句话说,在 fork A 处于活动状态(selectFork(A))时所做的所有更改仅记录在 fork A 的存储中,如果选择了另一个 fork,则不可用。 但是,测试合约本身(变量)中记录的更改仍然可用,因为测试合约是一个 _persistent_ 帐户。

selectFork cheatcodes将 remote 部分设置为分叉的数据源,但是 local 内存在分叉交换期间保持不变。 这也意味着可以使用任何分叉随时调用 selectFork,以设置_remote_ 数据源。 但是,重要的是要记住上述“读/写”访问规则始终适用,这意味着_writes_在分叉交换中是持久的。

例子

创建和选择分叉
contract ForkTest is Test {
    // the identifiers of the forks
    uint256 mainnetFork;
    uint256 optimismFork;
    
    //Access variables from .env file via vm.envString("varname")
    //Replace ALCHEMY_KEY by your alchemy key or Etherscan key, change RPC url if need
    //inside your .env file e.g: 
    //MAINNET_RPC_URL = 'https://eth-mainnet.g.alchemy.com//v2/ALCHEMY_KEY'
    //string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
    //string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL");

    // create two _different_ forks during setup
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        optimismFork = vm.createFork(OPTIMISM_RPC_URL);
    }

    // demonstrate fork ids are unique
    function testForkIdDiffer() public {
        assert(mainnetFork != optimismFork);
    }

    // select a specific fork
    function testCanSelectFork() public {
        // select the fork
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

        // from here on data is fetched from the `mainnetFork` if the EVM requests it and written to the storage of `mainnetFork`
    }

    // manage multiple forks in the same test
    function testCanSwitchForks() public {
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);

        vm.selectFork(optimismFork);
        assertEq(vm.activeFork(), optimismFork);
    }

    // forks can be created at all times
    function testCanCreateAndSelectForkInOneStep() public {
        // creates a new fork and also selects it
        uint256 anotherFork = vm.createSelectFork(MAINNET_RPC_URL);
        assertEq(vm.activeFork(), anotherFork);
    }

    // set `block.number` of a fork
    function testCanSetForkBlockNumber() public {
        vm.selectFork(mainnetFork);
        vm.rollFork(1_337_000);

        assertEq(block.number, 1_337_000);
    }
}

分离和持久存储(storage)

如前所述,每个分叉本质上都是一个独立的 EVM,具有独立的存储(storage)空间。

选择分叉时,只有 msg.sender 和测试合约(ForkTest)的账户是持久的。 但是任何帐户都可以变成持久帐户:makePersistent

persistent 帐户是唯一的, i.e.即它存在于所有分叉上

contract ForkTest is Test {
    // the identifiers of the forks
    uint256 mainnetFork;
    uint256 optimismFork;
    
    //Access variables from .env file via vm.envString("varname")
    //Replace ALCHEMY_KEY by your alchemy key or Etherscan key, change RPC url if need
    //inside your .env file e.g: 
    //MAINNET_RPC_URL = 'https://eth-mainnet.g.alchemy.com//v2/ALCHEMY_KEY'
    //string MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
    //string OPTIMISM_RPC_URL = vm.envString("OPTIMISM_RPC_URL");

    // create two _different_ forks during setup
    function setUp() public {
        mainnetFork = vm.createFork(MAINNET_RPC_URL);
        optimismFork = vm.createFork(OPTIMISM_RPC_URL);
    }

    // creates a new contract while a fork is active
    function testCreateContract() public {
        vm.selectFork(mainnetFork);
        assertEq(vm.activeFork(), mainnetFork);
        
        // the new contract is written to `mainnetFork`'s storage
        SimpleStorageContract simple = new SimpleStorageContract();
        
        // and can be used as normal
        simple.set(100);
        assertEq(simple.value(), 100);
        
        // after switching to another contract we still know `address(simple)` but the contract only lives in `mainnetFork` 
        vm.selectFork(optimismFork);
        
        /* this call will therefore revert because `simple` now points to a contract that does not exist on the active fork
        * it will produce following revert message:
        * 
        * "Contract 0xCe71065D4017F316EC606Fe4422e11eB2c47c246 does not exist on active fork with id `1`
        *       But exists on non active forks: `[0]`"
        */
        simple.value();
    }
    
     // creates a new _persistent_ contract while a fork is active
     function testCreatePersistentContract() public {
        vm.selectFork(mainnetFork);
        SimpleStorageContract simple = new SimpleStorageContract();
        simple.set(100);
        assertEq(simple.value(), 100);
        
        // mark the contract as persistent so it is also available when other forks are active
        vm.makePersistent(address(simple));
        assert(vm.isPersistent(address(simple))); 
        
        vm.selectFork(optimismFork);
        assert(vm.isPersistent(address(simple))); 
        
        // This will succeed because the contract is now also available on the `optimismFork`
        assertEq(simple.value(), 100);
     }
}

contract SimpleStorageContract {
    uint256 public value;

    function set(uint256 _value) public {
        value = _value;
    }
}

有关更多详细信息和示例,请参阅 forking cheatcodes 参考。