Rust
Testing with Mollusk

Testing with Mollusk

Advanced Functionalities

Advanced Functionalities

Mollusk provides flexible initialization options to accommodate different testing scenarios. You can create instances pre-loaded with your program or start with a minimal environment and add components as needed.

When testing a specific program, initialize Mollusk with your program pre-loaded:

use mollusk_svm::Mollusk;
use solana_sdk::pubkey::Pubkey;
 
const ID: Pubkey = solana_sdk::pubkey!("22222222222222222222222222222222222222222222");
 
#[test]
fn test() {
    let mollusk = Mollusk::new(&ID, "target/deploy/program.so");
}

This approach automatically loads your compiled program and makes it available for testing, streamlining the setup process for program-specific test suites.

For broader testing scenarios or when you need to add programs dynamically, start with the default instance:

use mollusk_svm::Mollusk;
 
#[test]
fn test() {
    // System Program, ...
    let mollusk = Mollusk::default();
}

The default instance includes essential builtin programs like the System Program, providing a foundation for most Solana operations without the overhead of programs you don't need.

When your tests require the System Program, Mollusk provides a convenient helper to generate the necessary account references:

let (system_program, system_program_account) = keyed_account_for_system_program();

To replicate program loading functionality or load custom programs that are not present by default, you can use these helpers:

use mollusk_svm::Mollusk;
use mollusk_svm::program::create_program_account_loader_v3;
 
#[test]
fn test() {
    let mut mollusk = Mollusk::default();
 
    // Get the account that you need
    let program = &ID; // ID of the program we're trying to load into mollusk
    let program_account = create_program_account_loader_v3(&ID);
 
    // Load the program into your mollusk instance
    mollusk.add_program(
        &ID, 
        "src/programs/program", 
        &mollusk_svm::program::loader_keys::LOADER_V3
    );
}

Token Program

Mollusk's token program helpers significantly simplify testing scenarios involving SPL tokens. The mollusk-svm-programs-token crate provides pre-configured support for Token, Token2022, and Associated Token programs.

Program Accounts

After including the token helper crate, add the specific token programs your tests require:

use mollusk_svm::Mollusk;
 
#[test]
fn test() {
    let mut mollusk = Mollusk::default();
 
    // Add the SPL Token Program
    mollusk_svm_programs_token::token::add_program(&mut mollusk);
 
    // Add the Token2022 Program
    mollusk_svm_programs_token::token2022::add_program(&mut mollusk);
 
    // Add the Associated Token Program
    mollusk_svm_programs_token::associated_token::add_program(&mut mollusk);
}

And create the account references needed for your test scenarios:

// SPL Token Program
let (token_program, token_program_account) = 
    mollusk_svm_programs_token::token::keyed_account();
 
// Token2022 Program
let (token2022_program, token2022_program_account) = 
    mollusk_svm_programs_token::token2022::keyed_account();
 
// Associated Token Program
let (associated_token_program, associated_token_program_account) =
    mollusk_svm_programs_token::associated_token::keyed_account();

These helpers ensure that token-related tests have access to the correct program accounts with proper configuration, enabling comprehensive testing of token operations without manual program setup.

State Accounts

During our tests, we might need to have a Mint, Token or Associated Token account that has already been initialized. Luckily for us, Mollusk has some handy helpers for us.

To create a Mint account we can use the create_account_for_mint() function like this:

use spl_token::state::Mint;
use mollusk_svm_programs_token::token::create_account_for_mint;
 
let mint_data = Mint {
    mint_authority: Pubkey::new_unique(),
    supply: 10_000_000_000,
    decimals: 6,
    is_initialized: true,
    freeze_authority: None,
};
 
let mint_account = create_account_for_mint(mint_data)

To create a Token account we can use the create_account_for_token_account() function like this:

use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::token::create_account_for_token_account;
 
let token_data = TokenAccount {
    mint: Pubkey::new_unique(),
    owner: Pubkey::new_unique(),
    amount: 1_000_000,
    delegate: None,
    state: AccountState::Initialized,
    is_native: None,
    delegated_amount: 0,
    close_authority: None,
};
 
let token_account = create_account_for_token_account(token_data)

Note: These examples are for the SPL-Token program. If you want to create Mint and Token account owned by the Token2022 program, just use mollusk_svm_programs_token::token2022::....

To create an Associated Token account we can use the create_account_for_associated_token_account() function like this:

use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::associated_token::pub fn create_account_for_associated_token_account;
 
let token_data = TokenAccount {
    mint: Pubkey::new_unique(),
    owner: Pubkey::new_unique(),
    amount: 1_000_000,
    delegate: None,
    state: AccountState::Initialized,
    is_native: None,
    delegated_amount: 0,
    close_authority: None,
};
 
let associated_token_account = create_account_for_associated_token_account(token_data)

Note: This example are for the SPL-Token program. If you want to create an Associated Token account owned by the Token2022 program, just use the create_account_for_associated_token_2022_account function.

Benchmarking Compute Units

Mollusk includes a dedicated compute unit benchmarking system that enables precise measurement and tracking of your program's computational efficiency. The MolluskComputeUnitBencher provides a streamlined API for creating comprehensive benchmarks that monitor compute unit consumption across different instruction scenarios.

This benchmarking system is particularly valuable for performance optimization, as it generates detailed reports showing both current compute unit usage and deltas from previous runs.

This allows you to immediately see the impact of code changes on your program's efficiency, helping you optimize critical performance bottlenecks.

The bencher integrates seamlessly with your existing Mollusk test setup:

use {
    mollusk_svm_bencher::MolluskComputeUnitBencher,
    mollusk_svm::Mollusk,
    /* ... */
};
 
// Optionally disable logging.
solana_logger::setup_with("");
 
/* Instruction & accounts setup ... */
 
let mollusk = Mollusk::new(&program_id, "my_program");
 
MolluskComputeUnitBencher::new(mollusk)
    .bench(("bench0", &instruction0, &accounts0))
    .bench(("bench1", &instruction1, &accounts1))
    .bench(("bench2", &instruction2, &accounts2))
    .bench(("bench3", &instruction3, &accounts3))
    .must_pass(true)
    .out_dir("../target/benches")
    .execute();

Configuration Options

The bencher provides several configuration options:

  • must_pass(true): Triggers a panic if any benchmark fails to execute successfully, ensuring your benchmarks remain valid as code changes
  • out_dir("../target/benches"): Specifies where the markdown report will be generated, allowing integration with CI/CD systems and documentation workflows

Integration with Cargo

To run benchmarks using cargo bench, add a benchmark configuration to your Cargo.toml:

[[bench]]
name = "compute_units"
harness = false

Benchmark Reports

The bencher generates markdown reports that provide both current performance metrics and historical comparison:

| Name   | CUs   | Delta  |
|--------|-------|--------|
| bench0 | 450   | --     |
| bench1 | 579   | -129   |
| bench2 | 1,204 | +754   |
| bench3 | 2,811 | +2,361 |

The report format includes:

  • Name: The benchmark identifier you specified
  • CUs: Current compute unit consumption for this scenario
  • Delta: Change from the previous benchmark run (positive indicates increased usage, negative indicates optimization)

Custom Syscalls

Mollusk supports the creation and testing of custom syscalls, enabling you to extend the Solana Virtual Machine with specialized functionality for testing scenarios.

This capability is particularly valuable for testing the creation of new Syscall that can be added through SIMD by simulating specific runtime behaviors, or creating controlled environments for testing.

Custom syscalls operate at the VM level, providing direct access to the invoke context and execution environment.

Defining Custom Syscalls

Custom syscalls are defined using the declare_builtin_function! macro, which creates a syscall that can be registered with Mollusk's runtime environment:

use {
    mollusk_svm::{result::Check, Mollusk},
    solana_instruction::Instruction,
    solana_program_runtime::{
        invoke_context::InvokeContext,
        solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping},
    },
    solana_pubkey::Pubkey,
};
 
declare_builtin_function!(
    /// A custom syscall to burn compute units for testing
    SyscallBurnCus,
    fn rust(
        invoke_context: &mut InvokeContext,
        to_burn: u64,
        _arg2: u64,
        _arg3: u64,
        _arg4: u64,
        _arg5: u64,
        _memory_mapping: &mut MemoryMapping,
    ) -> Result<u64, Box<dyn std::error::Error>> {
        // Consume the specified number of compute units
        invoke_context.consume_checked(to_burn)?;
        Ok(0)
    }
);

This is an example of a custom syscall that simply "burns" CUs.

The syscall function signature follows a specific pattern:

  • invoke_context: Provides access to the execution context and runtime state
  • Arguments 1-5: Up to five 64-bit arguments can be passed from the program
  • memory_mapping: Provides access to the program's memory space
  • Return value: A Result<u64, Box<dyn std::error::Error>> indicating success or failure

This how all Syscalls are created under the hood

Registering Custom Syscalls

Once defined, custom syscalls must be registered with Mollusk's program runtime environment before they can be used:

#[test]
fn test_custom_syscall() {
    std::env::set_var("SBF_OUT_DIR", "../target/deploy");
    let program_id = Pubkey::new_unique();
    
    let mollusk = {
        let mut mollusk = Mollusk::default();
        
        // Register the custom syscall with a specific name
        mollusk
            .program_cache
            .program_runtime_environment
            .register_function("sol_burn_cus", SyscallBurnCus::vm)
            .unwrap();
            
        // Add your program that uses the custom syscall
        mollusk.add_program(
            &program_id,
            "test_program_custom_syscall",
            &mollusk_svm::program::loader_keys::LOADER_V3,
        );
        
        mollusk
    };
}

The syscall is registered with a name ("sol_burn_cus" in this example) that your program can reference when making the syscall.

Testing Custom Syscall Behavior

Custom syscalls can be tested like any other program functionality, with the added benefit of precise control over their behavior:

fn instruction_burn_cus(program_id: &Pubkey, to_burn: u64) -> Instruction {
    Instruction::new_with_bytes(*program_id, &to_burn.to_le_bytes(), vec![])
}
 
#[test]
fn test_custom_syscall() {
    // ... mollusk setup ...
    
    // Establish baseline compute unit usage
    let base_cus = mollusk
        .process_and_validate_instruction(
            &instruction_burn_cus(&program_id, 0),
            &[],
            &[Check::success()],
        )
        .compute_units_consumed;
    
    // Test different compute unit consumption levels
    for to_burn in [100, 1_000, 10_000] {
        mollusk.process_and_validate_instruction(
            &instruction_burn_cus(&program_id, to_burn),
            &[],
            &[
                Check::success(),
                Check::compute_units(base_cus + to_burn), // Verify exact CU consumption
            ],
        );
    }
}

This example demonstrates testing a syscall that burns compute units, validating that the exact number of requested units are consumed. The ability to verify precise data makes Mollusk the best way to test custom syscalls before the implementation.

Configuration Methods

Mollusk provides comprehensive configuration options that allow you to customize the execution environment to match specific testing requirements as we can see from Mollusk Context:

/// Instruction context fixture.
pub struct Context {
    /// The compute budget to use for the simulation.
    pub compute_budget: ComputeBudget,
    /// The feature set to use for the simulation.
    pub feature_set: FeatureSet,
    /// The runtime sysvars to use for the simulation.
    pub sysvars: Sysvars,
    /// The program ID of the program being invoked.
    pub program_id: Pubkey,
    /// Accounts to pass to the instruction.
    pub instruction_accounts: Vec<AccountMeta>,
    /// The instruction data.
    pub instruction_data: Vec<u8>,
    /// Input accounts with state.
    pub accounts: Vec<(Pubkey, Account)>,
}

These configuration methods enable precise control over compute budgets, feature availability, and system variables, making it possible to test programs under various runtime conditions.

Basic Configuration Setup

use mollusk_svm::Mollusk;
use solana_sdk::feature_set::FeatureSet;
 
#[test]
fn test() {
    let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
    
    // Configure compute budget for performance testing
    mollusk.set_compute_budget(200_000);
    
    // Configure feature set to enable/disable specific Solana features
    mollusk.set_feature_set(FeatureSet::all_enabled());
    
    // Sysvars are handled automatically but can be customized if needed
}

The compute budget determines how many compute units are available for program execution. This is crucial for testing programs that approach or exceed compute limits:

// Test with standard compute budget
mollusk.set_compute_budget(200_000);

Solana's feature set controls which blockchain features are active during program execution. Mollusk allows you to configure these features to test compatibility across different network states:

use solana_sdk::feature_set::FeatureSet;
 
// Enable all features (latest functionality)
mollusk.set_feature_set(FeatureSet::all_enabled());
 
// Use default feature set (production-like environment)
mollusk.set_feature_set(FeatureSet::default());

For a comprehensive list of available features, consult the agave-feature-set crate documentation, which details all configurable blockchain features and their implications.

Mollusk provides access to all system variables (sysvars) that programs can query during execution. While these are automatically configured with reasonable defaults, you can customize them for specific testing scenarios:

/// Mollusk sysvars wrapper for easy manipulation
pub struct Sysvars {
    pub clock: Clock,                     // Current slot, epoch, and timestamp
    pub epoch_rewards: EpochRewards,      // Epoch reward distribution info  
    pub epoch_schedule: EpochSchedule,    // Epoch timing and slot configuration
    pub last_restart_slot: LastRestartSlot, // Last validator restart information
    pub rent: Rent,                       // Rent calculation parameters
    pub slot_hashes: SlotHashes,          // Recent slot hash history
    pub stake_history: StakeHistory,      // Historical stake activation data
}

You can customize specific sysvars to test time-dependent logic, rent calculations, or other system-dependent behaviors or use some of the helpers:

#[test]
fn test() {
    let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
    
    // Customize clock for time-based testing
    mollusk.sysvars.clock.epoch = 10;
    mollusk.sysvars.clock.unix_timestamp = 1234567890;
 
    // Jump to Slot 1000
    warp_to_slot(&mut Mollusk, 1000)
}
Contents
View Source
Blueshift © 2025Commit: e508535