Arbitrum Stylus logo

Stylus by Example

Unit Testing

Stylus unit tests run entirely in Rust, with a fully-mocked host—no EVM, no RPC, no gas. You can exercise pure logic, mock all host contexts, inspect side-effects, and even extend the VM to suit your needs.

1. HostAccess and vm()

Every Stylus contract automatically implements the HostAccess trait, giving you a .vm() handle inside your methods. Use self.vm() instead of global helpers so your code works both in WASM and in native unit tests:

1self.vm().msg_value()              // mocked msg.value() in tests
2self.vm().msg_sender()             // mocked msg.sender()
3self.vm().block_timestamp()        // mocked block timestamp
4
5// low-level external call (from inside your contract code)
6let ctx = Call::new_mutating(self);             // or Call::new() for view
7let ret = call(self.vm(), ctx, addr, &data);    // free function, not a VM method
1self.vm().msg_value()              // mocked msg.value() in tests
2self.vm().msg_sender()             // mocked msg.sender()
3self.vm().block_timestamp()        // mocked block timestamp
4
5// low-level external call (from inside your contract code)
6let ctx = Call::new_mutating(self);             // or Call::new() for view
7let ret = call(self.vm(), ctx, addr, &data);    // free function, not a VM method

In production WASM this maps to real host syscalls; in native tests it routes to TestVM or your custom host.

2. Basic Unit Test

With stylus_sdk::testing::* imported, write tests just like any Rust project. Below is a simple test suite for a counter contract that can be found at the bottom of the page.

1#[cfg(test)]
2mod test {
3  use super::*;
4  use stylus_sdk::testing::*;
5
6  #[test]
7  fn test_counter_basic() {
8    // 1) Create a TestVM and contract
9    let vm = TestVM::default();
10    let mut c = Counter::from(&vm);
11
12    // 2) Assert initial state
13    assert_eq!(c.number(), U256::ZERO);
14
15    // 3) Call methods and assert logic
16    c.increment();
17    assert_eq!(c.number(), U256::ONE);
18
19    // 4) Mock msg.value() and test payable fn
20    vm.set_value(U256::from(5));
21    c.add_from_msg_value();
22    assert_eq!(c.number(), U256::from(6));
23  }
24}
1#[cfg(test)]
2mod test {
3  use super::*;
4  use stylus_sdk::testing::*;
5
6  #[test]
7  fn test_counter_basic() {
8    // 1) Create a TestVM and contract
9    let vm = TestVM::default();
10    let mut c = Counter::from(&vm);
11
12    // 2) Assert initial state
13    assert_eq!(c.number(), U256::ZERO);
14
15    // 3) Call methods and assert logic
16    c.increment();
17    assert_eq!(c.number(), U256::ONE);
18
19    // 4) Mock msg.value() and test payable fn
20    vm.set_value(U256::from(5));
21    c.add_from_msg_value();
22    assert_eq!(c.number(), U256::from(6));
23  }
24}

Explanation

  • TestVM::default() seeds a clean in-memory VM, use it to create a new VM instance for each test, ensuring isolation
  • Counter::from(&vm) wires up storage against that VM
  • Calls like increment() and add_from_msg_value() run instantly—no blockchain needed
  • vm.set_value(...) overrides the msg.value() for that test
  • assert_eq!(...) checks the contract state after each call to verify logic

3. Inspecting & Mocking Host I/O

Stylus’s TestVM provides methods to override and inspect every host function. Use the table below as a quick reference:

Scenario

TestVM API

Override Ether attachedTestVM.set_value(U256)
Override Caller addressTestVM.set_sender(Address)
Read raw storage slotTestVM.storage_load_bytes32(slot)
Write raw storage slot & commitunsafe { TestVM.storage_cache_bytes32(slot, val) }; TestVM.flush_cache(false)
Override block parametersTestVM.set_block_number(n)
TestVM.set_block_timestamp(ts)
Inspect emitted logs & eventsTestVM.get_emitted_logs()
Mock external call responseTestVM.mock_call(addr, data, Ok(res)/Err(revert))

Explanation These methods let you simulate any on-chain context or inspect every side-effect your contract produces.

4. Event & Log Testing

To verify events and their indexed parameters you can use get_emitted_logs() to inspect the logs emitted by your contract. This method returns a list of (topics, data) pairs, where topics is a list of indexed parameters and data is the non-indexed data.

1#[test]
2fn test_event_emission() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5
6  // Trigger events
7  c.increment(); // may emit multiple logs
8
9  let logs = vm.get_emitted_logs();
10  assert_eq!(logs.len(), 2);
11
12  // First topic is the event signature
13  let sig: B256 = hex!(
14    "c9d64952459b33e1dd10d284fe1e9336b8c514cbf51792a888ee7615ca3225d9"
15  ).into();
16  assert_eq!(logs[0].0[0], sig);
17
18  // Indexed address is in topic[1], last 20 bytes
19  let mut buf = [0u8;20];
20  buf.copy_from_slice(&logs[0].0[1].into()[12..]);
21  assert_eq!(Address::from(buf), vm.msg_sender());
22}
1#[test]
2fn test_event_emission() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5
6  // Trigger events
7  c.increment(); // may emit multiple logs
8
9  let logs = vm.get_emitted_logs();
10  assert_eq!(logs.len(), 2);
11
12  // First topic is the event signature
13  let sig: B256 = hex!(
14    "c9d64952459b33e1dd10d284fe1e9336b8c514cbf51792a888ee7615ca3225d9"
15  ).into();
16  assert_eq!(logs[0].0[0], sig);
17
18  // Indexed address is in topic[1], last 20 bytes
19  let mut buf = [0u8;20];
20  buf.copy_from_slice(&logs[0].0[1].into()[12..]);
21  assert_eq!(Address::from(buf), vm.msg_sender());
22}

Explanation

  • get_emitted_logs() returns a list of (topics, data) pairs
  • Topics[0] is always the keccak-256 of the event signature
  • Subsequent topics hold indexed parameters, ABI-encoded

5. Mocking External Calls

Use mock_call to test cross-contract interactions without deploying dependencies. This powerful feature lets you simulate both successful responses and reverts from external contracts, allowing you to test your integration logic in complete isolation:

1#[test]
2fn test_external_call_behavior() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5
6  // Only owner may call
7  let owner = vm.msg_sender();
8  c.transfer_ownership(owner).unwrap();
9
10  let target = Address::from([5u8;20]);
11  let data   = vec![1,2,3];
12  let ok_ret = vec![7,7];
13  let err_ret= vec![9,9,9];
14
15  // 1) Successful call
16  vm.mock_call(target, data.clone(), Ok(ok_ret.clone()));
17  assert_eq!(c.call_external_contract(target, data.clone()), Ok(ok_ret));
18
19  // 2) Revert call
20  vm.mock_call(target, data.clone(), Err(err_ret.clone()));
21  let err = c.call_external_contract(target, data).unwrap_err();
22  let expected = format!("Revert({:?})", err_ret).as_bytes().to_vec();
23  assert_eq!(err, expected);
24}
1#[test]
2fn test_external_call_behavior() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5
6  // Only owner may call
7  let owner = vm.msg_sender();
8  c.transfer_ownership(owner).unwrap();
9
10  let target = Address::from([5u8;20]);
11  let data   = vec![1,2,3];
12  let ok_ret = vec![7,7];
13  let err_ret= vec![9,9,9];
14
15  // 1) Successful call
16  vm.mock_call(target, data.clone(), Ok(ok_ret.clone()));
17  assert_eq!(c.call_external_contract(target, data.clone()), Ok(ok_ret));
18
19  // 2) Revert call
20  vm.mock_call(target, data.clone(), Err(err_ret.clone()));
21  let err = c.call_external_contract(target, data).unwrap_err();
22  let expected = format!("Revert({:?})", err_ret).as_bytes().to_vec();
23  assert_eq!(err, expected);
24}

Explanation

  • mock_call(...) primes the VM to return Ok or Err for that address+input
  • Subsequent .call(&self, target, &data) picks up the mock

6. Raw Storage Testing

Directly inspect or override any storage slot. This is useful for testing storage layout, mappings, or verifying internal state after corner-case paths:

1#[test]
2fn test_storage_direct() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5  c.set_number(U256::from(42));
6
7  let slot = U256::ZERO;
8
9  // Read the underlying B256
10  assert_eq!(
11    vm.storage_load_bytes32(slot),
12    B256::from_slice(&U256::from(42).to_be_bytes::<32>())
13  );
14
15  // Overwrite slot
16  let new = U256::from(100);
17  unsafe { vm.storage_cache_bytes32(slot, B256::from_slice(&new.to_be_bytes::<32>())); }
18  vm.flush_cache(false);
19
20  // Verify via getter
21  assert_eq!(c.number(), new);
22}
1#[test]
2fn test_storage_direct() {
3  let vm = TestVM::new();
4  let mut c = Counter::from(&vm);
5  c.set_number(U256::from(42));
6
7  let slot = U256::ZERO;
8
9  // Read the underlying B256
10  assert_eq!(
11    vm.storage_load_bytes32(slot),
12    B256::from_slice(&U256::from(42).to_be_bytes::<32>())
13  );
14
15  // Overwrite slot
16  let new = U256::from(100);
17  unsafe { vm.storage_cache_bytes32(slot, B256::from_slice(&new.to_be_bytes::<32>())); }
18  vm.flush_cache(false);
19
20  // Verify via getter
21  assert_eq!(c.number(), new);
22}

Explanation

  • vm.storage_load_bytes32(slot) reads the raw bytes from the VM
  • vm.storage_cache_bytes32(slot, value) writes to the VM cache
  • vm.flush_cache(false) commits the cache to the VM

7. Block-Context Testing

Simulate block-dependent logic by overriding block number/timestamp. This is useful for testing timelocks, expiry logic, or height-based gating.

1#[test]
2fn test_block_dependent_logic() {
3  let vm: TestVM = TestVMBuilder::new()
4    .sender(my_addr)
5    .value(U256::ZERO)
6    .build();
7
8  let mut c = Counter::from(&vm);
9
10  vm.set_block_timestamp(1_234_567_890);
11  c.increment();  
12  assert_eq!(c.last_updated(), U256::from(1_234_567_890u64));
13
14  vm.set_block_timestamp(2_000_000_000);
15  c.increment();
16  assert_eq!(c.last_updated(), U256::from(2_000_000_000u64));
17}
1#[test]
2fn test_block_dependent_logic() {
3  let vm: TestVM = TestVMBuilder::new()
4    .sender(my_addr)
5    .value(U256::ZERO)
6    .build();
7
8  let mut c = Counter::from(&vm);
9
10  vm.set_block_timestamp(1_234_567_890);
11  c.increment();  
12  assert_eq!(c.last_updated(), U256::from(1_234_567_890u64));
13
14  vm.set_block_timestamp(2_000_000_000);
15  c.increment();
16  assert_eq!(c.last_updated(), U256::from(2_000_000_000u64));
17}

Explanation

  • TestVMBuilder seeds initial values; vm.set_* mutates them mid-test.
  • vm.set_block_timestamp(...) overrides the block timestamp
  • vm.set_block_number(...) overrides the block number

8. Customizing the VM

You can extend TestVM to add custom instrumentation or helpers without re-implementing the entire Host trait. This is useful for test-specific behaviors.

8-a. TestVMBuilder

Pre-seed context before deploying. This is useful for testing against a forked chain or pre-configured state:

1let vm: TestVM = TestVMBuilder::new()
2  .sender(my_addr)
3  .contract_address(ct_addr)
4  .value(U256::from(10))
5  .rpc_url("http://localhost:8547")  // fork real state
6  .build();
7
8vm.set_balance(my_addr, U256::from(1_000));
9vm.set_block_number(123);
1let vm: TestVM = TestVMBuilder::new()
2  .sender(my_addr)
3  .contract_address(ct_addr)
4  .value(U256::from(10))
5  .rpc_url("http://localhost:8547")  // fork real state
6  .build();
7
8vm.set_balance(my_addr, U256::from(1_000));
9vm.set_block_number(123);

8-b. Wrapping TestVM

Add new instrumentation without re-implementing Host. For example, you can track how many times mock_call was invoked. In this example, we create a CustomVM struct that wraps TestVM and adds a counter for the number of times mock_call is invoked.

1#[cfg(test)]
2mod custom_vm {
3  use super::*;
4  use stylus_sdk::testing::TestVM;
5  use alloy_primitives::Address;
6
7  pub struct CustomVM {
8    inner: TestVM,
9    pub mock_count: usize,
10  }
11
12  impl CustomVM {
13    pub fn new() -> Self { Self { inner: TestVM::default(), mock_count: 0 } }
14    pub fn mock_call(&mut self, tgt: Address, d: Vec<u8>, r: Result<_,_>) {
15      self.mock_count += 1;
16      self.inner.mock_call(tgt, d, r);
17    }
18    pub fn inner(&self) -> &TestVM { &self.inner }
19  }
20
21  #[test]
22  fn test_mock_counter() {
23    let mut vm = CustomVM::new();
24    let mut c  = Counter::from(vm.inner());
25
26    assert_eq!(vm.mock_count, 0);
27    let addr = Address::from([5u8;20]);
28    vm.mock_call(addr, vec![1], Ok(vec![7]));
29    assert_eq!(vm.mock_count, 1);
30  }
31}
1#[cfg(test)]
2mod custom_vm {
3  use super::*;
4  use stylus_sdk::testing::TestVM;
5  use alloy_primitives::Address;
6
7  pub struct CustomVM {
8    inner: TestVM,
9    pub mock_count: usize,
10  }
11
12  impl CustomVM {
13    pub fn new() -> Self { Self { inner: TestVM::default(), mock_count: 0 } }
14    pub fn mock_call(&mut self, tgt: Address, d: Vec<u8>, r: Result<_,_>) {
15      self.mock_count += 1;
16      self.inner.mock_call(tgt, d, r);
17    }
18    pub fn inner(&self) -> &TestVM { &self.inner }
19  }
20
21  #[test]
22  fn test_mock_counter() {
23    let mut vm = CustomVM::new();
24    let mut c  = Counter::from(vm.inner());
25
26    assert_eq!(vm.mock_count, 0);
27    let addr = Address::from([5u8;20]);
28    vm.mock_call(addr, vec![1], Ok(vec![7]));
29    assert_eq!(vm.mock_count, 1);
30  }
31}

Explanation

  • You never touch the sealed Host trait—simply delegate to TestVM.
  • Add any helper you like: call counters, argument recorders, custom fail patterns, etc.

8-c. Implementing Your Own TestVM

You can implement your own TestVM from scratch by implementing the Host trait from stylus_core::host::Host. This approach is useful for specialized testing scenarios or if you need complete control over the test environment.

9. Cheat-Sheet & Best Practices

What to test…

TestVM API

msg.value()vm.set_value(U256)
msg.sender()vm.set_sender(Address)
Raw storagevm.storage_load_bytes32(k)
unsafe { vm.storage_cache_bytes32(k, v) }; vm.flush_cache(false)
Block paramsvm.set_block_number(n)
vm.set_block_timestamp(ts)
Events & logsvm.get_emitted_logs()
External callsvm.mock_call(addr, data, Ok/Err)
Custom instrumentationWrap TestVM in your own struct and expose helpers
  • Group tests by feature or behavior
  • Keep contract logic pure; tests mock all side-effects
  • Use TestVMBuilder for forked or pre-configured state
  • Wrap TestVM to instrument or extend behavior without re-implementing Host

With these patterns, your Stylus unit suite will be fast, deterministic, and comprehensive—covering logic, host I/O, events, storage, and more.

10. Complete Test Coverage Example

The contract below demonstrates a comprehensive test suite that covers all aspects of Stylus contract testing:

  • Basic state manipulation tests
  • Event emission verification
  • External call mocking (both success and failure cases)
  • Direct storage slot access
  • Time-dependent logic with block context manipulation
  • Access control and ownership tests
  • Edge case handling and error conditions
  • Custom test VM extensions for advanced instrumentation

Each test demonstrates a different aspect of the testing framework, from simple value assertions to complex mock interactions. The example counter contract is intentionally designed with features that exercise all major testing capabilities.

You can use this pattern as a template for your own comprehensive test suites, ensuring your Stylus contracts are thoroughly verified before deployment.

src/lib.rs

1Loading...
1Loading...

Cargo.toml

1Loading...
1Loading...

src/main.rs

1Loading...
1Loading...