分叉(Fork)测试

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

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

分叉模式

要在分叉环境(例如分叉的以太坊主网)中运行所有测试,请通过 --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

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

缓存(Caching)

如果同时指定了 --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 环境变量。

分叉作弊码

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

用法

重要的是要记住,_所有_测试函数都是隔离的,这意味着每个测试函数都使用 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 作弊码将 remote 部分设置为分叉的数据源,但是 本地(local) 内存在分叉更换期间保持不变。 这也意味着可以使用任何分叉随时调用 selectFork,以设置_remote_ 数据源。 但是,重要的是要记住上述 读/写访问规则始终适用,这意味着_写_在分叉更换中是持久的。

例子

创建和选择分叉
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 帐户是唯一的, 即它存在于所有分叉上

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 作弊码 参考。