单元测试 - EVM
对于使用Foundry的多链设置来说,单元测试可能具有挑战性。因此,我们提供了一个轻量级的测试环境 MockHyperlaneEnvironment
,让你能够对跨链应用进行单元测试,而无需分叉多个网络。
大多数多链应用将基于我们的Mailbox合约构建。因此,我们使用MockMailbox
抽象了已部署邮箱的细节,我们的环境在同一条链上包含originMailbox
和destinationMailbox
。在内部,我们将到达目的地的消息存储在目的地邮箱的inboundMessages
映射中。我们通过将消息入队并使用MockMailbox.processNextInboundMessage()
增加inboundProcessedNonce
来模拟消息传递。
简单消息传递forge测试的设置如下:
发送消息
contract SimpleMessagingTest is Test {
// 源链和目标链域名(建议使用链ID)
uint32 origin = 1;
uint32 destination = 2;
// 两个邮箱将在同一条链上但地址不同
MockMailbox originMailbox;
MockMailbox destinationMailbox;
// 可以接收消息的合约
TestRecipient receiver;
function setUp() public {
originMailbox = new MockMailbox(origin);
destinationMailbox = new MockMailbox(destination);
originMailbox.addRemoteMailbox(destination, destinationMailbox);
receiver = new TestRecipient();
}
function testSendMessage() public {
string _message = "Aloha!";
originMailbox.dispatch(
destination,
TypeCasts.addressToBytes32(address(receiver)),
bytes(_message)
);
// simulating message delivery to the destinationMailbox
destinationMailbox.processNextInboundMessage();
assertEq(string(receiver.lastData()), _message);
}
}
测试基于 Router 的应用
假设你正在测试继承自Router
的TestCrosschainApp
:
contract CrosschainAppTest is Test {
// 源链和目标链域名(建议使用链ID)
uint32 origin = 1;
uint32 destination = 2;
function setUp() public {
environment = new MockHyperlaneEnvironment(origin, destination);
// 你的跨链应用
TestCrosschainApp originTelephone = new TestCrosschainApp(environment.mailboxes(origin));
TestCrosschainApp destinationTelephone = new TestCrosschainApp(environment.mailboxes(destination));
// 假设你正在继承来自以下地址的Router模式
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/contracts/client/Router.sol
originTelephone.enrollRemoteRouter(destinationTelephone);
destinationTelephone.enrollRemoteRouter(originTelephone);
}
}
调用processNextPendingMessage()
和processNextPendingMessageFromDestination()
分别处理目的地和源邮箱的入站消息。现在,你可以进行从源到目的地以及反向的跨链调用:
function testRemoteTelephoneCallFromOrigin() public {
// 检查源链上的行为
vm.expectEmit(true, true, true, false);
emit TelephoneRinging(destination, TypeCasts.bytes32ToAddress(destinationTelephone), "Hello!"); // 源链上的示例事件
originTelephone.callRemote(destination, TypeCasts.bytes32ToAddress(destinationTelephone), "Hello!");
// 模拟消息从源链传递到目标链
environment.processNextPendingMessage();
// 检查目标链上的行为
assertEq(destinationTelephone.latestMessage(originTelephone) == "Hello!");
}
function testRemoteTelephoneCallFromDestination() public {
// 检查目标链上的行为
vm.expectEmit(true, true, true, false);
emit TelephoneRinging(origin, TypeCasts.bytes32ToAddress(originTelephone), "Howdy!"); // 目标链上的示例事件
destinationTelephone.callRemote(origin, TypeCasts.bytes32ToAddress(originTelephone), "Howdy!");
// 模拟消息从目标链传递到源链
environment.processNextPendingMessageFromDestination();
// 检查源链上的行为
assertEq(originTelephone.latestMessage(destinationTelephone) == "Howdy!");
}
如果你想为你的应用使用自己的 ISM,你可以通过将其传递给 Router 的 initialize
方法来覆盖邮箱提供的 defaultIsm
,如下所示:
contract CrosschainAppTest is Test {
// 源链和目标链域名(建议使用链ID)
uint32 origin = 1;
uint32 destination = 2;
function setUp() public {
...
TestIgp igp = new TestIgp(); // 作为钩子传递的跨链Gas支付主合约示例
// 部署你自己的ISM合约来验证originTelephone和destinationTelephone之间的消息
TelephoneISM originIsm = new TelephoneISM(); // 源链的本地ISM
TelephoneISM destinationIsm = new TelephoneISM(); // 目标链的本地ISM
originTelephone.initialize(address(igp), address(originIsm), msg.sender);
originTelephone.initialize(address(igp), address(destinationIsm), msg.sender);
...
}
}
提示
你可以在这里找到我们单元测试设置的示例:InterchainAccountRouterTest