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.
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 method1self.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 methodIn production WASM this maps to real host syscalls; in native tests it routes to TestVM or your custom host.
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 isolationCounter::from(&vm) wires up storage against that VMincrement() and add_from_msg_value() run instantly—no blockchain neededvm.set_value(...) overrides the msg.value() for that testassert_eq!(...) checks the contract state after each call to verify logicStylus’s TestVM provides methods to override and inspect every host function. Use the table below as a quick reference:
Scenario | TestVM API |
|---|---|
| Override Ether attached | TestVM.set_value(U256) |
| Override Caller address | TestVM.set_sender(Address) |
| Read raw storage slot | TestVM.storage_load_bytes32(slot) |
| Write raw storage slot & commit | unsafe { TestVM.storage_cache_bytes32(slot, val) }; TestVM.flush_cache(false) |
| Override block parameters | TestVM.set_block_number(n)TestVM.set_block_timestamp(ts) |
| Inspect emitted logs & events | TestVM.get_emitted_logs() |
| Mock external call response | TestVM.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.
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) pairsUse 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.call(&self, target, &data) picks up the mockDirectly 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 VMvm.storage_cache_bytes32(slot, value) writes to the VM cachevm.flush_cache(false) commits the cache to the VMSimulate 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 timestampvm.set_block_number(...) overrides the block numberYou can extend TestVM to add custom instrumentation or helpers without re-implementing the entire Host trait. This is useful for test-specific behaviors.
TestVMBuilderPre-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);TestVMAdd 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
Host trait—simply delegate to 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.
What to test… | TestVM API |
|---|---|
| msg.value() | vm.set_value(U256) |
| msg.sender() | vm.set_sender(Address) |
| Raw storage | vm.storage_load_bytes32(k)unsafe { vm.storage_cache_bytes32(k, v) }; vm.flush_cache(false) |
| Block params | vm.set_block_number(n)vm.set_block_timestamp(ts) |
| Events & logs | vm.get_emitted_logs() |
| External calls | vm.mock_call(addr, data, Ok/Err) |
| Custom instrumentation | Wrap TestVM in your own struct and expose helpers |
TestVMBuilder for forked or pre-configured stateTestVM to instrument or extend behavior without re-implementing HostWith these patterns, your Stylus unit suite will be fast, deterministic, and comprehensive—covering logic, host I/O, events, storage, and more.
The contract below demonstrates a comprehensive test suite that covers all aspects of Stylus contract testing:
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.
1Loading...1Loading...1Loading...1Loading...1Loading...1Loading...