Table of contents
- Introduction
- What Makes Motsu Special?
- Setting Up Your Project
- Writing Your First Test
- Accounts and Addresses
- Interacting with Contracts
- Testing Events
- Handling Reverts
- Testing Contract-to-Contract Interactions
- Manipulating the VM Environment
- Practical Example: Testing an ERC-20 Token
- Tips for Effective Testing
- Conclusion
Introduction
As Ethereum scaling solutions mature, Arbitrum Stylus stands out as a fascinating innovation that lets developers write smart contracts in Rust. This offers significant performance improvements and brings Rust's robust type system and memory safety guarantees to the blockchain world.
But as any experienced smart contract developer knows, thorough testing is non-negotiable. That's where Motsu comes in – a testing framework designed specifically for Stylus contracts that feels both familiar to Rust developers and conceptually similar to tools like Hardhat and Foundry that Solidity developers know well.
In this tutorial, we'll explore how to effectively test your Stylus contracts using Motsu. If you're coming from a Solidity background with experience in tools like Foundry or Hardhat, you'll find many parallels that will help you get up to speed quickly.
You can find the complete working project with all examples in our GitHub repository here: https://github.com/0xNeshi/motsu-tutorial
What Makes Motsu Special?
Motsu addresses a core challenge in testing Stylus smart contracts: simulating the blockchain environment. Just as Hardhat provides a JavaScript environment for testing Solidity contracts, and Foundry provides a Solidity-native testing experience, Motsu delivers a pure Rust testing experience for Stylus contracts by mocking the vm affordances. Unlike Hardhat or Foundry, which spin up lightweight blockchain nodes to execute tests, Motsu takes a different approach by directly intercepting and mocking Stylus host functions at the Wasm level. This enables fast, isolated tests without requiring a full blockchain runtime, making it ideal for unit testing Stylus contracts purely in Rust.
The name "Motsu" (持つ, Japanese for "to hold") cleverly references "holding a stylus in our hand" – a fitting metaphor for a tool that gives you control over your Stylus contract tests.
Setting Up Your Project
Before diving into Motsu, make sure your project is properly configured. Below are the dependencies that we’re going to need for this project:
Let’s add the contract in our src/lib.rs:
Writing Your First Test
Let's start with a simple example. If you've written tests in Rust before, the structure will look familiar, with one key difference: instead of using #[test], we use #[motsu::test].
Here's a basic test for our Vault contract:Let's break down what's happening here:
- The #[motsu::test] attribute transforms your test function to provide access to the VM environment
- Parameters to the test function are automatically injected:
- contract: Contract<Vault> creates an instance of your contract
- alice: Address creates a test address
This is conceptually similar to how Foundry's setUp() function prepares your test environment, but in a more Rust-idiomatic way through function parameters.
Accounts and Addresses
Motsu provides two types for representing blockchain accounts:
- Address - A simple Ethereum address (like Ethereum's address type)
- Account - An address with an associated private key for signing operations
You can use either as parameters in your test functions:Note that you need to add alloy-signer@0.11.1 to your [dev-dependencies] in Cargo.toml to be able to perform signing operations.
Unless you need to access the private key or the underlying signer in your tests we recommend using Address as it is more lightweight.
Deterministic Accounts with Tags
When debugging tests, having consistently named accounts can help track issues. Motsu provides a FromTag trait that is included in its prelude to create deterministic addresses from string identifiers, and this trait is what’s used to inject test parameters:
Interacting with Contracts
The core of Motsu testing revolves around the Contract<T> type, which represents a deployed instance of your smart contract.
Basic Interactions
To call functions on your contract:The sender() method sets the caller of the transaction, similar to Hardhat's connect() or Foundry's vm.prank().
Passing Value with Calls
Imagine we added a payable deposit function to our Vault that accepts the underlying gas token:For payable functions, you can send ETH using sender_and_value().
Testing Events
Smart contracts often emit events that you'll want to verify in your tests.
First, let’s update our deposit function to emit an event on a successful deposit:Motsu provides an elegant way to check emitted events:
In case of a failing event assertion, Motsu provides well formatted panic messages with account/contract addresses substituted with appropriate tags, if tags were used to instantiate them.
Handling Reverts
Testing failure scenarios is just as important as testing successful operations. Let’s add a decrease_balance function to Vault that reverts with an error on balance underflow:Motsu provides several methods to handle reverts properly:
The key methods for handling reverts are:
- motsu_unwrap() - Unwraps the result or panics with error details
- motsu_unwrap_err() - Expects an error and returns it, or panics if successful
- motsu_expect() - Like unwrap() but with a custom message
- motsu_expect_err() - Like unwrap_err() but with a custom message
- motsu_res() - Reverts the transaction on error but returns the Result
These methods ensure that the contract state is properly reverted when transactions fail, similar to how real blockchain transactions behave. In case any of the above methods panic, the panic messages will be pretty-printed for clarity, with addresses being replaced with appropriate tags if that’s how they were instantiated.
Testing Contract-to-Contract Interactions
One of Motsu's powerful features is the ability to test interactions between multiple contracts. This is essential for complex DeFi applications or any system with multiple interacting components.
Let’s set up a simple proxy contract implementation:Motsu handles the internal contract-to-contract wiring, allowing us to easily test the above proxy:
Manipulating the VM Environment
Sometimes you need to simulate specific blockchain conditions. Motsu lets you modify certain environment variables like chain ID, which lets you simulate a chain fork.
Practical Example: Testing an ERC-20 Token
Let's put everything together by testing an ERC-20 token implementation.
We’ll inherit openzeppelin-stylus library’s ERC-20 implementation, so let’s add the necessary dependency to our Cargo.toml:We can now open src/lib.rs and implement our ERC-20 token together with a comprehensive transfer test:
Tips for Effective Testing
Based on the patterns we've explored, here are some best practices for testing with Motsu:
- Use deterministic accounts with tags for easier debugging
- Test both success and failure cases using the appropriate motsu_* functions
- Verify events to ensure your contract is communicating state changes correctly
- Test complex interactions between multiple contracts
- Check balances before and after operations that involve value transfers
- Use account types appropriately - Address for simple cases, Account when signing is needed
- Remember to implement TopLevelStorage for contracts without #[entrypoint]
Conclusion
Motsu provides a powerful, Rust-native testing environment for Arbitrum Stylus contracts. While conceptually similar to tools like Hardhat and Foundry, it leverages Rust's type system to provide a more integrated testing experience.
For developers coming from Solidity, the mental model is quite transferable - you're still working with contracts, addresses, and transactions. But Motsu's design takes advantage of Rust features to make tests more concise and easier to reason about.
As you continue developing with Stylus, keep exploring Motsu's capabilities. The time invested in writing thorough tests will pay dividends in the reliability and security of your smart contracts.
Happy testing!