Learn how to test Soroban contracts using the testutils feature
The Soroban SDK provides comprehensive testing utilities through the testutils feature, enabling you to write thorough unit tests for your smart contracts.
Test that your contract correctly reads and writes storage:
#[cfg(test)]mod test { use super::*; use soroban_sdk::{symbol_short, Env}; #[test] fn test_storage() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Store a value client.put(&symbol_short!("key"), &symbol_short!("value")); // Retrieve the value let result = client.get(&symbol_short!("key")); assert_eq!(result, Some(symbol_short!("value"))); // Delete the value client.del(&symbol_short!("key")); assert_eq!(client.get(&symbol_short!("key")), None); }}
Verify that your contract publishes events correctly:
use soroban_sdk::testutils::Events;#[test]fn test_events() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); let from = Address::generate(&env); let to = Address::generate(&env); let amount = 1000i128; client.transfer(&from, &to, &amount); // Get all events let events = env.events().all(); // Verify event was published assert_eq!(events.len(), 1);}
For contracts using #[contractevent], you can compare against the event struct directly:
use soroban_sdk::Event;#[test]fn test_event_struct() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); let from = Address::generate(&env); let to = Address::generate(&env); let amount = 1i128; client.transfer(&from, &to, &amount); // Compare using the event struct assert_eq!( env.events().all(), std::vec![Transfer { from: from.clone(), to: to.clone(), amount, memo: None, } .to_xdr(&env, &contract_id)], );}
use soroban_sdk::testutils::{Address as _, MockAuth};#[test]fn test_auth() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Generate a test address let user = Address::generate(&env); // Mock authentication for all addresses let result = client.mock_all_auths().protected_function(&user); assert_eq!(result, expected_value); // Verify auth was required assert_eq!( env.auths(), std::vec![( user.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( contract_id.clone(), Symbol::new(&env, "protected_function"), (&user,).into_val(&env) )), sub_invocations: std::vec![] } )] );}
Use try_ prefixed methods to test error cases without panicking:
use soroban_sdk::InvokeError;#[test]fn test_error_handling() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); // Test successful case let result = client.try_hello(&Flag::A); assert_eq!(result, Ok(Ok(symbol_short!("hello")))); // Test contract error let result = client.try_hello(&Flag::B); assert_eq!(result, Err(Ok(Error::AnError))); // Test panic/abort let result = client.try_hello(&Flag::D); assert_eq!(result, Err(Err(InvokeError::Abort)));}
When a contract function panics, any storage changes are rolled back. Test this behavior using the try_ methods.
use soroban_sdk::testutils::Ledger;#[test]fn test_with_ledger_state() { let env = Env::default(); // Set ledger timestamp env.ledger().set_timestamp(1000); // Set sequence number env.ledger().set_sequence_number(100); // Test time-dependent functionality // ...}
use soroban_sdk::testutils::{Address as _, BytesN as _};#[test]fn test_with_random_data() { let env = Env::default(); // Generate random address let addr = Address::generate(&env); // Generate random bytes let random_bytes = BytesN::<32>::random(&env); // Use in tests // ...}