Testing Your Module

In this section, we'll walk you through writing tests for the ValueSetter module you've been working on. The Sovereign SDK provides a powerful testing framework in the sov-test-utils crate that allows you to test your module's logic in an isolated environment, without needing to run a full rollup.

Step 1: Setting Up the Test Environment

All module tests follow a similar pattern. First, we need to create a temporary, isolated runtime that includes our module. Then, for each test, we'll define the initial ("genesis") state and use a TestRunner to execute transactions and make assertions.

Let's build a setup helper function to handle this boilerplate.

a) Create a Test Runtime

The first thing we need is a runtime to test against. The generate_optimistic_runtime! macro creates a temporary runtime that includes your ValueSetter module alongside the core modules (like the Bank) needed for a functioning rollup.

// Typically in tests/test_value_setter.rs
use sov_modules_api::Spec;
use sov_test_utils::{generate_optimistic_runtime, TestSpec};
use value_setter::{ValueSetter, ValueSetterConfig};

type S = TestSpec;

// This macro creates a temporary runtime for testing.
generate_optimistic_runtime!(
    TestRuntime <=
    value_setter: ValueSetter<S>
);

b) Create a setup Helper

To avoid repeating code in every test, we'll create a setup function. This function will be responsible for creating test users, configuring the initial state of the rollup (the genesis state), and initializing the TestRunner that we'll use to drive the tests.

use sov_test_utils::runtime::genesis::optimistic::HighLevelOptimisticGenesisConfig;
use sov_test_utils::runtime::TestRunner;
use sov_test_utils::TestUser;

// A helper struct to hold our test users, for convenience.
pub struct TestData<S: Spec> {
    pub admin: TestUser<S>,
    pub regular_user: TestUser<S>,
}

pub fn setup() -> (TestData<S>, TestRunner<TestRuntime<S>, S>) {
    // Create two users, the first of which will be our admin.
    // (The `HighLevelOptimisticGenesisConfig` builder is a convenient way
    // to set up the initial state for core modules.)
    let genesis_config = HighLevelOptimisticGenesisConfig::generate()
        .add_accounts_with_default_balance(2);
    
    let mut users = genesis_config.additional_accounts().to_vec();
    let regular_user = users.pop().unwrap();
    let admin = users.pop().unwrap();

    let test_data = TestData {
        admin: admin.clone(),
        regular_user,
    };

    // Configure the genesis state for our ValueSetter module.
    let value_setter_config = ValueSetterConfig {
        admin: admin.address(),
    };

    // Build the final genesis config by combining
    // the core config with our module's specific config.
    let genesis = GenesisConfig::from_minimal_config(
        genesis_config.into(),
        value_setter_config,
    );
    
    // Initialize the TestRunner with the genesis state.
    // The runner gives us a simple way to execute transactions and query state.
    let runner = TestRunner::new_with_genesis(
        genesis.into_genesis_params(),
        TestRuntime::default(),
    );
    
    (test_data, runner)
}

This setup function now gives us a freshly initialized test environment for every test case, with our admin and a regular_user ready to go.

Step 2: Writing a "Happy Path" Test

Now, let's write our first test to ensure the admin can successfully set the value. We use a TransactionTestCase to bundle the transaction input with a set of assertions to run after execution.

use sov_test_utils::{AsUser, TransactionTestCase};
use value_setter::{CallMessage, Event};

#[test]
fn test_admin_can_set_value() {
    // 1. Setup
    let (test_data, mut runner) = setup();
    let admin = &test_data.admin;

    let new_value = 42;

    // 2. Execute the transaction
    runner.execute_transaction(TransactionTestCase {
        // The transaction input, created by the admin user.
        input: admin.create_plain_message::<TestRuntime<S>, ValueSetter<S>>(
            CallMessage::SetValue(new_value),
        ),
        // The assertions to run after execution.
        assert: Box::new(move |result, state| {
            // 3. Assert the outcome
            assert!(result.tx_receipt.is_successful());

            // Assert that the correct event was emitted.
            assert_eq!(result.events.len(), 1);
            let event = &result.events[0];
            // Note: The event enum name (`TestRuntimeEvent`) is auto-generated by our `generate_optimistic_runtime!` macro.
            assert_eq!(
                event,
                &TestRuntimeEvent::ValueSetter(Event::ValueUpdated(new_value))
            );

            // Assert that the state was updated correctly by querying the module.
            let value_setter = ValueSetter::<S>::default();
            let current_value = value_setter.value.get(state).unwrap();
            assert_eq!(current_value, Some(new_value));
        }),
    });
}

Step 3: Testing a Failure Case

It's equally important to test that our module fails when it should. Let's add a test to ensure a regular user cannot set the value.

#[test]
fn test_regular_user_cannot_set_value() {
    // 1. Setup
    let (test_data, mut runner) = setup();
    let regular_user = &test_data.regular_user;

    // 2. Execute the transaction from the non-admin user
    runner.execute_transaction(TransactionTestCase {
        // This time we're sending the transaction from the regular_user
        input: regular_user.create_plain_message::<TestRuntime<S>, ValueSetter<S>>(
            CallMessage::SetValue(99),
        ),
        assert: Box::new(move |result, state| {
            // 3. Assert that the transaction was reverted
            assert!(result.tx_receipt.is_reverted());
            
            // Optional: Check for the specific error message
            if let sov_modules_api::TxEffect::Reverted(err) = result.tx_receipt {
                assert!(err.reason.to_string().contains("Only the admin can set the value."));
            }

            // Assert that the state was NOT changed.
            let value_setter = ValueSetter::<S>::default();
            let current_value = value_setter.value.get(state).unwrap();
            assert_eq!(current_value, None); // It should remain un-set.
        }),
    });
}

Step 4: Running Your Tests

Execute your tests from your module's root directory using the standard Cargo command:

cargo test

Additional Testing Capabilities

The TestRunner provides methods for more advanced scenarios, all documented in the sov-test-utils crate. Key capabilities include:

  • Batch Execution: Execute and assert on a sequence of transactions with runner.execute_batch(...).
  • Time Advancement: Test time-sensitive logic (like in Hooks) by advancing the slot count with runner.advance_slots(...).
  • Historical Queries: Query state at a specific block height with runner.query_state_at_height(...).
  • API Testing: Run an integrated REST API server for off-chain testing with runner.query_api_response(...).

What's Next?

With a thoroughly tested module, you can be confident that your on-chain logic is correct. The next step is to understand how users will interact with it from the outside world.

In the next chapter, "Wallets and Accounts," we'll take a closer look at how users create accounts, sign transactions, and submit them to your rollup.