ModuleKit
Test
Integration testing

Integration testing

The ModuleKit comes with a set of tools to simplify and accelerate the testing of your modules. This page provides the necessary information to use the ModuleKit's integration testing framework.

Setup

To start integration testing, you need to import the following objects:

import {
    RhinestoneModuleKit,
    ModuleKitHelpers,
    ModuleKitUserOp,
    AccountInstance,
    UserOpData
} from "modulekit/ModuleKit.sol";

Then, you should set up your test contract to inherit from RhinestoneModuleKit and the Foundry Test contract. Use the ModuleKitHelpers and ModuleKitUserOp libraries to simplify the testing process. You should also create an instance of the account you want to test. Here's an example of how to set up your test contract to test a Validator module on an AccountInstance instance:

contract IntegrationTestExample is RhinestoneModuleKit, Test {
    using ModuleKitHelpers for *;
    using ModuleKitUserOp for *;
 
    // account instance
    AccountInstance internal instance;
 
    function setUp() public {
        init();
 
        // Your test setup
 
        // Create the account and deal it some ether
        instance = makeAccountInstance("IntegrationTestExample");
        vm.deal(address(instance.account), 10 ether);
    }
 
    ...
}

If you want more custom control over the initial Modules on your account instance, you can use the makeAccountInstance(bytes32 salt, ERC7579BootstrapConfig[] memory validators, ERC7579BootstrapConfig[] memory executors, ERC7579BootstrapConfig memory hook, ERC7579BootstrapConfig memory fallBack) function. This requires you to import ERC7579BootstrapConfig from "modulekit/external/ERC7579.sol"; For example:

function _setupAccount() public {
    ERC7579BootstrapConfig[] memory validators =
        makeBootstrapConfig(address(ownableValidator), abi.encode(owner.addr));
    ERC7579BootstrapConfig[] memory executors =
        makeBootstrapConfig(address(flashloanCallback), abi.encode(""));
    ERC7579BootstrapConfig memory hook = _emptyConfig();
    ERC7579BootstrapConfig memory fallBack =
        _makeBootstrapConfig(address(auxiliary.fallbackHandler), abi.encode(params));
    instance = makeAccountInstance("mainAccount", validators, executors, hook, fallBack);
}

Note that this uses the helpers _makeBootstrapConfig and _emptyConfig which come with the ModuleKit.

Execution

Simple

For a simple execution, you can just use one of the following functions:

instance.exec(address target, bytes memory callData);
instance.exec(address target, uint256 value, bytes memory callData);

This will trigger a UserOperation on instance.account with the correctly formatted data and using the default validator (which accepts every UserOperation).

Advanced

For more advanced executions, you can use one of the following functions to get a UserOpData struct:

instance.getExecOps(address target, uint256 value, bytes memory callData, address txValidator);
instance.getExecOps(Execution[] memory executions, address txValidator);
instance.getExecOps(address[] memory targets, uint256[] memory values, bytes[] memory callDatas, address txValidator);

This will return a UserOpData struct, which consists of the userOp and the userOpHash.

To execute the UserOperation, simply call:

userOpData.execUserOps();

In between getting the UserOpData and executing it, you may perform any computation or changes to the userOp, such as adding a signature for a specific validator that you are using. If you do not perform any steps in between, you can also simply call:

instance.getExecOps(...).execUserOps();

Config

To configure modules on the account, the ModuleKit exposes the following helper functions:

instance.installModule(uint256 moduleTypeId, address module, bytes memory data);
instance.unInstallModule(uint256 moduleTypeId, address module, bytes memory data);
instance.isModuleInstalled(uint256 moduleTypeId, address module, bytes memory data);

You can also use the imported MODULE_TYPE_[MODULE_TYPE_NAME] to make it easier to pass the moduleTypeId argument (eg MODULE_TYPE_VALIDATOR). These can be imported from modulekit/external/ERC7579.sol.

Utilities

Expecting a revert

To expect an ERC-4337 transaction to revert, similar to Foundry's vm.expectRevert(), you can call:

instance.expect4337Revert();

Note, the reason why vm.expectRevert() wont work in this case is that if the UserOperation reverts during the execution phase, the batch will still go through as a successful transaction, but the EntryPoint will emit an event saying the specific UserOperation reverted. The ModuleKit parses this event and determines if the UserOperation reverted or not and it will throw an error if it reverted, unless instance.expect4337Revert() was called in advance.