12 releases

0.10.0-rc.1 Oct 17, 2025
0.10.0-beta.1 Aug 15, 2025
0.10.0-beta.0 Jul 18, 2025
0.9.2 Sep 23, 2025
0.8.1 Feb 13, 2025

#3 in #arbitrum

Download history 783/week @ 2025-07-26 791/week @ 2025-08-02 1054/week @ 2025-08-09 1176/week @ 2025-08-16 948/week @ 2025-08-23 899/week @ 2025-08-30 814/week @ 2025-09-06 634/week @ 2025-09-13 1038/week @ 2025-09-20 675/week @ 2025-09-27 657/week @ 2025-10-04 1160/week @ 2025-10-11 1006/week @ 2025-10-18 1004/week @ 2025-10-25 1105/week @ 2025-11-01 820/week @ 2025-11-08

4,550 downloads per month
Used in 5 crates (via stylus-sdk)

MIT/Apache

77KB
968 lines

Stylus Test

The stylus-test crate makes it easy to unit test all the storage types and contracts that use the Stylus SDK. Included is an implementation of the stylus_core::host::Host trait that all Stylus contracts have access to for interfacing with their host environment.

The mock implementation, named TestVM, can be used to unit test Stylus contracts in native Rust without the need for a real EVM or Arbitrum chain environment. The TestVM allows for mocking of all host functions, including storage, gas, and external calls to assert contract behavior.

To be able to unit test Stylus contracts, contracts must access host methods through the HostAccess trait, which gives all contracts access to a .vm() method. That is, instead of calling stylus_sdk::msg::value() directly, contracts should do self.vm().msg_value().

Getting Started

The stylus-test crate is not meant to be used directly, as it is already exported by the Stylus SDK. It can be accessed via stylus_sdk::testing::*. Here is how to use it for a basic Counter smart contract defined in the stylus-hello-world template:

use stylus_sdk::{alloy_primitives::U256, prelude::*};

sol_storage! {
    #[entrypoint]
    pub struct Counter {
        uint256 number;
    }
}

#[public]
impl Counter {
    pub fn number(&self) -> U256 {
        self.number.get()
    }
    pub fn increment(&mut self) {
        let number = self.number.get();
        self.set_number(number + U256::from(1));
    }
    #[payable]
    pub fn add_from_msg_value(&mut self) {
        let number = self.number.get();
        self.set_number(number + self.vm().msg_value());
    }
}

After defining the contract above using the Stylus SDK, we can define native Rust unit tests as are commonly written in Rust projects:

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_counter() {
        use stylus_sdk::testing::*;
        ...
    }
}

The stylus_sdk::testing::* import gives your tests access to a powerful TestVM struct which lets you mock almost any part of your contracts' host environment, such as the the message value of a transaction, or even raw storage. Here's how it works:

// We define a default TestVM. Note that we can customize it via a TestVMBuilder
// we will discuss later in this README.
let vm = TestVM::default();

// You can then initialize your Counter Stylus contract from a VM reference, as all
// Stylus storage types implement the `From<VM>` trait.
let mut contract = Counter::from(&vm);

Next, we can perform some basic assertions.

// The TestVM handles the internals of contract storage, so normal
// storage interactions will work as expected in your test.
assert_eq!(U256::ZERO, contract.number());
contract.increment();
assert_eq!(U256::from(1), contract.number());

Next, we can mock the message value of the transaction, and assert some of our contract's logic.

// Override the msg value.
vm.set_value(U256::from(2));

contract.add_from_msg_value();

// Assert the value is as we expect.
assert_eq!(U256::from(3), contract.number());

TestVM Custom Builder

The TestVM default is easy to use, but one can further initialize a TestVM from custom values, even using an external RPC endpoint to fork storage reads.

stylus_sdk::testing::TestVMBuilder Allows for convenient customization of the contract's address, sender address, message value, and RPC URL if state forking is desired. These values and more can still be customized if the builder is not used, by instead invoking the corresponding method on the TestVM struct such as vm.set_msg_value(value).

use stylus_test::{TestVM, TestVMBuilder};
use alloy_primitives::{address, Address, U256};

let vm: TestVM = TestVMBuilder::new()
    .sender(address!("dCE82b5f92C98F27F116F70491a487EFFDb6a2a9"))
    .contract_address(address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"))
    .value(U256::from(1))
    .rpc_url("http://localhost:8547")
    .build();

Inspecting Emitted Logs and Mocking Calls

Logs emitted can also be inspected by the TestVM pattern:

#[test]
fn test_logs() {
    let vm = TestVM::new();
    let topic1 = B256::from([1u8; 32]);
    let topic2 = B256::from([2u8; 32]);
    let data = vec![3, 4, 5];

    vm.raw_log(&[topic1, topic2], &data).unwrap();

    let logs = vm.get_emitted_logs();
    assert_eq!(logs.len(), 1);
    assert_eq!(logs[0].0, vec![topic1, topic2]);
    assert_eq!(logs[0].1, data);
}

Calls can easily be mocked:

#[test]
fn test_mock_calls() {
    let vm = TestVM::new();
    let target = Address::from([2u8; 20]);
    let data = vec![1, 2, 3, 4];
    let expected_return = vec![5, 6, 7, 8];

    // Mock a regular call.
    vm.mock_call(target, data.clone(), Ok(expected_return.clone()));

    let ctx = stylus_core::calls::Call::new();
    let result = vm.call(&ctx, target, &data).unwrap();
    assert_eq!(result, expected_return);

    // Mock an error case.
    let error_data = vec![9, 9, 9];
    vm.mock_call(target, data.clone(), Err(error_data.clone()));

    match vm.call(&ctx, target, &data) {
        Err(Error::Revert(returned_data)) => assert_eq!(returned_data, error_data),
        _ => panic!("Expected revert error"),
    }
}

Writing your Own Custom TestVM

A TestVM is a simple struct implemented in the stylus-test crate that implements the Host trait from stylus_core::host::Host. Anyone can implement the trait and allow for rich testing experiences for Stylus contracts. The TestVM is not the only way to unit test your projects, as you can extend or implement your own.

Dependencies

~42–60MB
~1M SLoC