Unit Test
Unit Test a Smart Contract
chaijs
https://docs.alchemy.com/docs/how-to-unit-test-a-smart-contract
Foundry forge
中文:https://learnblockchain.cn/docs/foundry/i18n/zh/forge/writing-tests.html
原文:https://book.getfoundry.sh/forge/writing-tests
AccountExecHooksTest.sol
这段代码是一个智能合约测试脚本,用于测试UpgradeableModularAccount合约中与执行钩子(hooks)相关的功能。这个测试脚本是使用foundry框架编写的,foundry是一个以太坊智能合约开发工具,它包含测试框架、合约编译器和部署工具。
测试脚本AccountExecHooksTest继承自OptimizedTest,它使用了foundry的vm对象来模拟区块链环境,进行合约调用、设置预期的事件(event)发出、处理状态变更等。
这个测试脚本的主要目的是验证UpgradeableModularAccount合约中的执行钩子是否按照预期工作。执行钩子允许在执行特定函数之前和之后运行自定义逻辑。这些钩子通常用于验证、状态更新、事件记录等。
测试脚本中的关键部分包括:
-
setUp():在每个测试开始前运行,用于设置测试环境,包括部署所需的合约实例,如
EntryPoint、SingleOwnerPlugin、MSCAFactoryFixture和UpgradeableModularAccount。 -
test_preExecHook_install():测试安装具有前置执行钩子的插件。
-
test_preExecHook_run():测试执行具有前置执行钩子的插件,验证钩子是否被正确调用。
-
test_preExecHook_uninstall():测试卸载具有前置执行钩子的插件,确保卸载后 钩子不再被调用。
-
test_execHookPair_install():测试安装具有成对的执行钩子(前置和后置)的插件。
-
test_execHookPair_run():测试执行具有成对执行钩子的插件,验证前置和后置钩子是否按顺序正确调用。
-
test_execHookPair_uninstall():测试卸载具有成对执行钩子的插件。
-
test_postOnlyExecHook_install():测试安装仅具有后置执行钩子的插件。
-
test_postOnlyExecHook_run():测试执行仅具有后置执行钩子的插件。
-
test_postOnlyExecHook_uninstall():测试卸载仅具有后置执行钩子的插件。
-
test_overlappingPreExecHooks_install():测试安装两个具有相同前置执行钩子的插件。
-
test_overlappingPreExecHooks_run():测试执行具有重叠前置执行钩子的插件。
-
test_overlappingPreExecHooks_uninstall():测试卸载具有重叠前置执行钩子的插件。
-
test_execHookDependencies_failInstall():测试插件依赖关系,确保当插件具有不满足的依赖时安装会失败。
-
_installPlugin1WithHooks()、_installPlugin2WithHooksExpectSuccess()、_installPlugin2WithHooksExpectFail() 和 _uninstallPlugin():这些是辅助函数,用于在测试中安装和卸载插件,同时检查预期的事件是否发生。
这个测试脚本是对智能合约开发的一个很好的实践示例,它展示了如何通过自动化测试来确保合约的各个组件按照预期工作。通过这种方式,开发者可以在合约部署到主网之前捕捉到潜在的错误和问题。
测试:期望事件的数据匹配
test_preExecHook_install
function test_preExecHook_install() public {
// _installPlugin1WithHooks(bytes4 selector, ManifestFunction preHook, ManifestFunction postHook)
_installPlugin1WithHooks(
_EXEC_SELECTOR,
ManifestFunction({
functionType: ManifestAssociatedFunctionType.SELF,
functionId: _PRE_HOOK_FUNCTION_ID_1,
dependencyIndex: 0
}),
ManifestFunction({functionType: ManifestAssociatedFunctionType.NONE, functionId: 0, dependencyIndex: 0})
);
}
// 执行一个安装函数,该函数会触发两个事件
function _installPlugin1WithHooks(
bytes4 selector,
ManifestFunction memory preHook,
ManifestFunction memory postHook
) internal {
m1.executionHooks.push(ManifestExecutionHook(selector, preHook, postHook));
mockPlugin1 = new MockPlugin(m1);
manifestHash1 = keccak256(abi.encode(mockPlugin1.pluginManifest()));
// 测试预期
vm.expectEmit(true, true, true, true);
emit ReceivedCall(abi.encodeCall(IPlugin.onInstall, (bytes(""))), 0);
// 测试预期安装完成
vm.expectEmit(true, true, true, true);
emit PluginInstalled(address(mockPlugin1), manifestHash1, new FunctionReference[](0));
// 执行安装
// @file: src/account/UpgradeableModularAccount.sol
account.installPlugin({
plugin: address(mockPlugin1),
manifestHash: manifestHash1,
pluginInstallData: bytes(""),
dependencies: new FunctionReference[](0)
});
}
installPlugin
/// @inheritdoc IPluginManager
function installPlugin(
address plugin,
bytes32 manifestHash,
bytes calldata pluginInstallData,
FunctionReference[] calldata dependencies
) external override wrapNativeFunction {
// @file: src/account/PluginManagerInternals.sol
_installPlugin(plugin, manifestHash, pluginInstallData, dependencies);
}
_installPlugin
function _installPlugin(
address plugin,
bytes32 manifestHash,
bytes memory pluginInstallData,
FunctionReference[] memory dependencies
) internal{
// ....
// Initialize the plugin storage for the account.
// 初始化插件的存储
// solhint-disable-next-line no-empty-blocks
try IPlugin(plugin).onInstall(pluginInstallData) {}
catch (bytes memory revertReason) {
revert PluginInstallCallbackFailed(plugin, revertReason);
}
emit PluginInstalled(plugin, manifestHash, dependencies);
}
多事件测试匹配
如果您期望在合约执行的过程中发出多个不同的事件,您需要为每个事件分别设置 vm.expectEmit 来定义对应的预期。对于每个预期的事件,您都需要指定期望的 topics 和 data 是否应该匹配。
例如,如果您的合约在某个函数调用中按顺序发出了两个不同的事件 EventA 和 EventB,那么您的测试代码可能会像这样:
// 设置对第一个事件的预期
vm.expectEmit(true, true, false, false);
emit EventA(expectedParam1, expectedParam2); // 预期收到的事件,前两个参数对的上
// 调用合约的函数,该函数将触发事件
contract.functionThatEmitsEvents();
// 设置对第二个事件的预期
vm.expectEmit(false, false, true, true);
emit EventB(expectedParam3, expectedParam4); // 预期收到的事件,后两个参数对的上
在这个例子中,vm.expectEmit 被调用了两次,每次都是在调用引发事件的函数之前。每个 vm.expectEmit 调用都设置了对接下来将要发出的特定事件的预期。这样,测试框架就可以检查实际发出的事件是否符合这些预期。如果任何一个事件不符合预期,测试将会失败。
test_preExecHook_run
function setUp() public {
entryPoint = new EntryPoint();
singleOwnerPlugin = _deploySingleOwnerPlugin();
factory = new MSCAFactoryFixture(entryPoint, singleOwnerPlugin);
// Create an account with "this" as the owner, so we can execute along the runtime path with regular
// solidity semantics
account = factory.createAccount(address(this), 0);
m1.executionFunctions.push(_EXEC_SELECTOR);
m1.runtimeValidationFunctions.push(
ManifestAssociatedFunction({
executionSelector: _EXEC_SELECTOR,
associatedFunction: ManifestFunction({
functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW,
functionId: 0,
dependencyIndex: 0
})
})
);
}
function test_preExecHook_run() public {
test_preExecHook_install();
vm.expectEmit(true, true, true, true);
emit ReceivedCall(
abi.encodeWithSelector(
IPlugin.preExecutionHook.selector,
_PRE_HOOK_FUNCTION_ID_1, // functionId
address(this), // caller
0, // msg.value in call to account
abi.encodeWithSelector(_EXEC_SELECTOR) // 下面对应这个函数,这个函数之前会调用 preExecutionHook
),
0 // msg value in call to plugin
);
// 这里执行了一个函数调用,调用的是 account 合约的 _EXEC_SELECTOR 函数
(bool success,) = address(account).call(abi.encodeWithSelector(_EXEC_SELECTOR)); // 这里是执行了一个函数调用
assertTrue(success);
}
MSCAFactoryFixture createAccount
function createAccount(address owner, uint256 salt) public returns (UpgradeableModularAccount) {
address addr = Create2.computeAddress(getSalt(owner, salt), _PROXY_BYTECODE_HASH);
// short circuit if exists
if (addr.code.length == 0) {
address[] memory plugins = new address[](1);
plugins[0] = address(singleOwnerPlugin);
bytes32[] memory pluginManifestHashes = new bytes32[](1);
pluginManifestHashes[0] = keccak256(abi.encode(singleOwnerPlugin.pluginManifest()));
bytes[] memory pluginInstallData = new bytes[](1);
pluginInstallData[0] = abi.encode(owner);
// not necessary to check return addr since next call will fail if so
new ERC1967Proxy{salt: getSalt(owner, salt)}(address(accountImplementation), "");
// point proxy to actual implementation and init plugins
UpgradeableModularAccount(payable(addr)).initialize(plugins, pluginManifestHashes, pluginInstallData);
}
return UpgradeableModularAccount(payable(addr));
}
UpgradeableModularAccount _doPreExecHooks 取钩子遍历执行
function _doPreExecHooks(bytes4 selector, bytes calldata data)
internal
returns (PostExecToRun[] memory postHooksToRun)
{
HookGroup storage hooks = getAccountStorage().selectorData[selector].executionHooks;
uint256 preExecHooksLength = hooks.preHooks.length();
uint256 postOnlyHooksLength = hooks.postOnlyHooks.length();
uint256 maxPostExecHooksLength = postOnlyHooksLength;
// There can only be as many associated post hooks to run as there are pre hooks.
for (uint256 i = 0; i < preExecHooksLength;) {
(, uint256 count) = hooks.preHooks.at(i);
unchecked {
maxPostExecHooksLength += (count + 1);
++i;
}
}
// Overallocate on length - not all of this may get filled up. We set the correct length later.
postHooksToRun = new PostExecToRun[](maxPostExecHooksLength);
uint256 actualPostHooksToRunLength;
// Copy post-only hooks to the array.
for (uint256 i = 0; i < postOnlyHooksLength;) {
(bytes32 key,) = hooks.postOnlyHooks.at(i);
postHooksToRun[actualPostHooksToRunLength].postExecHook = _toFunctionReference(key);
unchecked {
++actualPostHooksToRunLength;
++i;
}
}
// Then run the pre hooks and copy the associated post hooks (along with their pre hook's return data) to
// the array.
for (uint256 i = 0; i < preExecHooksLength;) {
(bytes32 key,) = hooks.preHooks.at(i);
FunctionReference preExecHook = _toFunctionReference(key);
if (preExecHook.isEmptyOrMagicValue()) {
// The function reference must be PRE_HOOK_ALWAYS_DENY in this case, because zero and any other
// magic value is unassignable here.
revert AlwaysDenyRule();
}
// 这里执行 preExecHook
bytes memory preExecHookReturnData = _runPreExecHook(preExecHook, data);
uint256 associatedPostExecHooksLength = hooks.associatedPostHooks[preExecHook].length();
if (associatedPostExecHooksLength > 0) {
for (uint256 j = 0; j < associatedPostExecHooksLength;) {
(key,) = hooks.associatedPostHooks[preExecHook].at(j);
postHooksToRun[actualPostHooksToRunLength].postExecHook = _toFunctionReference(key);
postHooksToRun[actualPostHooksToRunLength].preExecHookReturnData = preExecHookReturnData;
unchecked {
++actualPostHooksToRunLength;
++j;
}
}
}
unchecked {
++i;
}
}
// Trim the post hook array to the actual length, since we may have overallocated.
assembly ("memory-safe") {
mstore(postHooksToRun, actualPostHooksToRunLength)
}
}
ResultConsumerPlugin(address(account))
function test_returnData_execFromPlugin_fallback() public {
// 这里调用了插件的函数,但是调用者是 account,而不是插件。这是因为 account 是一个 UpgradeableModularAccount 合约实例,它实现了 IPluginExecutor 接口。所以它可以调用 IPluginExecutor 的 executeFromPlugin 函数。
// checkResultEFPFallback 是 ResultConsumerPlugin 的一个函数,它接收了一个 bytes32 类型的参数,返回一个 bool 类型的结果。
// ResultConsumerPlugin将 address(account) 作为参数传递给了 ResultCreatorPlugin 的 checkResultEFPFallback 函数。
// ???啥玩意
bool result = ResultConsumerPlugin(address(account)).checkResultEFPFallback(keccak256("bar"));
assertTrue(result);
}
ResultConsumerPlugin
contract ResultConsumerPlugin is BaseTestPlugin {
ResultCreatorPlugin public immutable resultCreator;
RegularResultContract public immutable regularResultContract;
constructor(ResultCreatorPlugin _resultCreator, RegularResultContract _regularResultContract) {
resultCreator = _resultCreator;
regularResultContract = _regularResultContract;
}
// Check the return data through the executeFromPlugin fallback case
// 通过executeFromPlugin回退案例检查返回数据
function checkResultEFPFallback(bytes32 expected) external returns (bool) {
// 这行代码意味着调用者(msg.sender)是一个实现了IPluginExecutor接口的合约。
IPluginExecutor(msg.sender).executeFromPlugin(abi.encodeCall(ResultCreatorPlugin.foo, ()));
// 这里调用了插件的函数,但是调用者是 account,而不是插件。这是因为 account 是一个 UpgradeableModularAccount 合约实例,它实现了 IPluginExecutor 接口。所以它可以调用 IPluginExecutor 的 executeFromPlugin 函数。
bytes32 actual = ResultCreatorPlugin(msg.sender).foo();
return actual == expected;
}
// Check the rturn data through the executeFromPlugin std exec case
// 通过executeFromPlugin std exec案例检查返回数据
function checkResultEFPExternal(address target, bytes32 expected) external returns (bool) {
// This result should be allowed based on the manifest permission request
// 这个结果应该是允许的,因为它符合插件清单的权限请求
bytes memory returnData = IPluginExecutor(msg.sender).executeFromPluginExternal(
target, 0, abi.encodeCall(RegularResultContract.foo, ())
);
// 把数据进行解析了,拿到数据
bytes32 actual = abi.decode(returnData, (bytes32));
return actual == expected;
}
function onInstall(bytes calldata) external override {}
function onUninstall(bytes calldata) external override {}
function pluginManifest() external pure override returns (PluginManifest memory) {
}
function _innerPluginManifest() internal view returns (PluginManifest memory) {
}
}