Skip to main content
The Soroban SDK provides comprehensive testing utilities through the testutils feature, enabling you to write thorough unit tests for your smart contracts.

Setup

Add the SDK with testutils to your dev dependencies:
Cargo.toml
[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }

Basic Test Structure

Here’s a simple contract with tests:
#![no_std]
use soroban_sdk::{contract, contractimpl};

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn add(a: u64, b: u64) -> u64 {
        a + b
    }
}

#[cfg(test)]
mod test {
    use soroban_sdk::Env;
    use crate::{Contract, ContractClient};

    #[test]
    fn test_add() {
        // Create a test environment
        let env = Env::default();
        
        // Register the contract
        let contract_id = env.register(Contract, ());
        
        // Create a client
        let client = ContractClient::new(&env, &contract_id);

        // Call contract functions
        let result = client.add(&10u64, &12u64);
        
        // Assert results
        assert_eq!(result, 22);
    }
}
The ContractClient is automatically generated by the SDK based on your #[contractimpl] block.

Test Environment

The Env type is the core of contract testing:
1

Create an environment

let env = Env::default();
2

Register your contract

// Without constructor
let contract_id = env.register(Contract, ());

// With constructor arguments
let contract_id = env.register(
    Contract,
    ContractArgs::__constructor(&100_u32, &1000_i64)
);
3

Create a client and invoke functions

let client = ContractClient::new(&env, &contract_id);
let result = client.my_function(&arg1, &arg2);

Testing Storage

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);
    }
}

Testing with Constructor

Test contracts that use the __constructor function:
#[test]
fn test_constructor() {
    let env = Env::default();
    
    // Register with constructor arguments
    let contract_id = env.register(
        Contract,
        ContractArgs::__constructor(&100_u32, &1000_i64)
    );
    
    let client = ContractClient::new(&env, &contract_id);
    
    // Verify constructor initialized state correctly
    assert_eq!(client.get_data(&DataKey::Persistent(100)), Some(1000));
    assert_eq!(client.get_data(&DataKey::Temp(200)), Some(2000));
    assert_eq!(client.get_data(&DataKey::Instance(300)), Some(3000));
}

Testing Events

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)],
    );
}

Testing Authentication

Test contracts that require authorization:
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![]
            }
        )]
    );
}

Testing Error Handling

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.

Advanced Testing Features

Ledger State

Modify ledger state for testing:
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
    // ...
}

Budget Tracking

Monitor resource consumption:
use soroban_sdk::testutils::budget;

#[test]
fn test_resource_usage() {
    let env = Env::default();
    env.cost_estimate().budget().reset_default();
    
    // Run contract operations
    let contract_id = env.register(Contract, ());
    let client = ContractClient::new(&env, &contract_id);
    client.expensive_operation();
    
    // Check resource usage
    let budget = env.cost_estimate().budget();
    println!("CPU instructions: {}", budget.cpu_instruction_cost());
    println!("Memory bytes: {}", budget.memory_bytes_cost());
}

Random Test Data

Generate random addresses and data:
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
    // ...
}

Best Practices

Always test boundary conditions, empty inputs, and maximum values.
Name tests to clearly describe what they verify:
#[test]
fn test_transfer_fails_with_insufficient_balance() { }
Each test should create its own Env and not depend on other tests.
Verify that storage is in the correct state before and after operations.

Running Tests

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run a specific test
cargo test test_add

Next Steps

Error Handling

Learn to handle and test errors

Deploying Contracts

Deploy your tested contracts