Files
2025-11-30 09:01:25 +08:00

30 KiB

Solana Program Testing Frameworks

Detailed guide for Mollusk, LiteSVM, and Anchor testing frameworks

This file provides comprehensive documentation for the main testing frameworks used in Solana program development. For an overview of the testing strategy and pyramid, see the related files.



Table of Contents

  1. Mollusk Testing
  2. Anchor-Specific Testing
  3. Native Rust Testing

Mollusk Testing

What is Mollusk?

Mollusk is a lightweight test harness that provides a minified Solana Virtual Machine (SVM) environment for program testing. It creates a program execution pipeline directly from low-level SVM components without the overhead of a full validator.

Key characteristics:

  • No validator runtime (no AccountsDB, Bank, or other large components)
  • Exceptionally fast test execution
  • Direct program ELF execution via BPF Loader
  • Requires explicit account lists (can't load from storage)
  • Configurable compute budget, feature set, and sysvars

Setup and Dependencies

Version Compatibility

IMPORTANT: Mollusk versions must match your Solana SDK version.

For Anchor 0.32.1 (Solana SDK 2.2.x):

[dev-dependencies]
mollusk-svm = "0.5.1"
mollusk-svm-bencher = "0.5.1"
mollusk-svm-programs-token = "0.5.1"
solana-sdk = "2.2"
spl-token = "7.0"
spl-associated-token-account = "6.0"

Why 0.5.1?

  • Anchor 0.32.1 uses Solana SDK 2.2.x internally
  • Mollusk 0.5.1 is the last version compatible with Solana 2.x
  • Mollusk 0.6.0+ uses Solana 3.0 and won't compile with Anchor 0.32.1

For Native Rust programs (Solana SDK 2.1.x or 2.2.x):

[dev-dependencies]
mollusk-svm = "0.5.1"
solana-sdk = "2.2"  # Or "2.1" depending on your program

For newer Solana versions (3.0+):

[dev-dependencies]
mollusk-svm = "0.9"  # Latest version
solana-sdk = "3.0"

How to check your Solana SDK version:

# For Anchor projects
grep solana-program programs/*/Cargo.toml

# For native Rust
grep solana-program Cargo.toml

# Check Anchor's internal SDK version
cargo tree | grep solana-sdk

Standard Dependencies

For testing with Token program:

[dev-dependencies]
mollusk-svm-programs-token = "0.5.1"  # Match mollusk-svm version
spl-token = "7.0"                      # For Solana 2.x

For compute unit benchmarking:

[dev-dependencies]
mollusk-svm-bencher = "0.5.1"  # Match mollusk-svm version

Basic Test Structure

use {
    mollusk_svm::Mollusk,
    solana_sdk::{
        account::Account,
        instruction::{AccountMeta, Instruction},
        pubkey::Pubkey,
    },
};

#[test]
fn test_my_instruction() {
    // 1. Initialize Mollusk with your program
    let program_id = Pubkey::new_unique();
    let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");

    // 2. Setup accounts
    let user = Pubkey::new_unique();
    let accounts = vec![
        (user, Account {
            lamports: 1_000_000,
            data: vec![],
            owner: program_id,
            executable: false,
            rent_epoch: 0,
        }),
    ];

    // 3. Create instruction
    let instruction = Instruction::new_with_bytes(
        program_id,
        &[0, 1, 2, 3],  // instruction data
        vec![AccountMeta::new(user, true)],
    );

    // 4. Process instruction
    let result = mollusk.process_instruction(&instruction, &accounts);

    // 5. Assert success
    assert!(result.is_ok());
}

Four Main API Methods

Mollusk provides four core testing methods:

1. process_instruction - Execute single instruction, return result

let result = mollusk.process_instruction(&instruction, &accounts);

2. process_and_validate_instruction - Execute and validate with checks

mollusk.process_and_validate_instruction(
    &instruction,
    &accounts,
    &checks,
);

3. process_instruction_chain - Execute multiple instructions sequentially

let result = mollusk.process_instruction_chain(
    &[instruction1, instruction2, instruction3],
    &accounts,
);

4. process_and_validate_instruction_chain - Execute chain with per-instruction checks

mollusk.process_and_validate_instruction_chain(
    &[
        (&instruction1, &[Check::success()]),
        (&instruction2, &[Check::success()]),
    ],
    &accounts,
);

Creating Test Accounts

Test accounts must be created explicitly with all required fields:

use solana_sdk::account::Account;

// Basic account
let account = Account {
    lamports: 1_000_000,           // Account balance
    data: vec![0; 100],             // Account data
    owner: program_id,              // Owner program
    executable: false,              // Not executable
    rent_epoch: 0,                  // Rent epoch
};

// System account
let system_account = Account {
    lamports: 1_000_000,
    data: vec![],
    owner: system_program::id(),
    executable: false,
    rent_epoch: 0,
};

// Rent-exempt account
let rent = mollusk.sysvars.rent;
let rent_exempt_account = Account {
    lamports: rent.minimum_balance(data_len),
    data: vec![0; data_len],
    owner: program_id,
    executable: false,
    rent_epoch: 0,
};

Processing Instructions

Simple execution:

let result = mollusk.process_instruction(&instruction, &accounts);
assert!(result.is_ok());

With result inspection:

let result = mollusk.process_instruction(&instruction, &accounts);
match result {
    Ok(result) => {
        println!("Compute units: {}", result.compute_units_consumed);
        // Access modified accounts from result
    }
    Err(err) => panic!("Instruction failed: {:?}", err),
}

Validation with Check API

The Check enum provides common validation patterns:

Success checks:

use mollusk_svm::result::Check;

let checks = vec![
    Check::success(),                          // Instruction succeeded
    Check::compute_units(5000),                // Exact compute units
];

Account state checks:

let checks = vec![
    Check::account(&pubkey)
        .lamports(1_000_000)                   // Check lamports
        .data(&[1, 2, 3, 4])                   // Check full data
        .data_slice(8, &[1, 2, 3, 4])          // Check data slice at offset
        .owner(&program_id)                     // Check owner
        .executable(false)                      // Check executable flag
        .space(100)                             // Check data length
        .rent_exempt()                          // Check rent exempt
        .build(),
];

Error checks:

use solana_sdk::instruction::InstructionError;

let checks = vec![
    Check::instruction_err(InstructionError::InvalidInstructionData),
];

Complete validation example:

use {
    mollusk_svm::{Mollusk, result::Check},
    solana_sdk::{
        account::Account,
        instruction::Instruction,
        pubkey::Pubkey,
        system_instruction,
        system_program,
    },
};

#[test]
fn test_system_transfer() {
    let sender = Pubkey::new_unique();
    let recipient = Pubkey::new_unique();

    let base_lamports = 100_000_000;
    let transfer_amount = 42_000;

    let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);
    let accounts = [
        (
            sender,
            Account::new(base_lamports, 0, &system_program::id()),
        ),
        (
            recipient,
            Account::new(base_lamports, 0, &system_program::id()),
        ),
    ];

    let checks = vec![
        Check::success(),
        Check::account(&sender)
            .lamports(base_lamports - transfer_amount)
            .build(),
        Check::account(&recipient)
            .lamports(base_lamports + transfer_amount)
            .build(),
    ];

    Mollusk::default().process_and_validate_instruction(
        &instruction,
        &accounts,
        &checks,
    );
}

Compute Unit Benchmarking

Monitor compute unit usage to catch performance regressions:

Basic benchmark:

use mollusk_svm_bencher::MolluskComputeUnitBencher;

fn main() {
    let program_id = Pubkey::new_unique();
    let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");

    MolluskComputeUnitBencher::new(mollusk)
        .bench(("my_instruction", &instruction, &accounts))
        .must_pass(true)
        .out_dir("./target/benches")
        .execute();
}

Benchmark multiple instructions:

fn main() {
    let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
    let bencher = MolluskComputeUnitBencher::new(mollusk);

    bencher.bench(("initialize", &init_ix, &init_accounts))
        .must_pass(true);

    bencher.bench(("update", &update_ix, &update_accounts))
        .must_pass(true);

    bencher.bench(("close", &close_ix, &close_accounts))
        .must_pass(true)
        .out_dir("./target/benches")
        .execute();
}

Run benchmarks with:

cargo bench

Output includes:

  • Current compute units consumed
  • Previous benchmark value
  • Delta (increase/decrease)
  • Pass/fail status

Advanced Patterns

Stateful Context Testing

Use MolluskContext to persist account state across multiple instructions:

use std::collections::HashMap;

#[test]
fn test_sequential_transfers() {
    let mollusk = Mollusk::default();

    // Create initial account store
    let mut account_store = HashMap::new();
    let alice = Pubkey::new_unique();
    let bob = Pubkey::new_unique();

    account_store.insert(
        alice,
        Account {
            lamports: 1_000_000,
            data: vec![],
            owner: system_program::id(),
            executable: false,
            rent_epoch: 0,
        },
    );

    account_store.insert(
        bob,
        Account {
            lamports: 0,
            data: vec![],
            owner: system_program::id(),
            executable: false,
            rent_epoch: 0,
        },
    );

    // Create stateful context
    let context = mollusk.with_context(account_store);

    // First transfer - state persists automatically
    let instruction1 = system_instruction::transfer(&alice, &bob, 200_000);
    context.process_instruction(&instruction1);

    // Second transfer - uses updated state from first transfer
    let instruction2 = system_instruction::transfer(&alice, &bob, 100_000);
    context.process_instruction(&instruction2);

    // Access final account state
    let store = context.account_store.borrow();
    assert_eq!(store.get(&alice).unwrap().lamports, 700_000);
    assert_eq!(store.get(&bob).unwrap().lamports, 300_000);
}

Instruction Chains with Validation

Process multiple instructions and validate state after each:

#[test]
fn test_instruction_chain_with_checks() {
    let mollusk = Mollusk::default();

    let alice = Pubkey::new_unique();
    let bob = Pubkey::new_unique();
    let carol = Pubkey::new_unique();

    let starting_lamports = 1_000_000;

    mollusk.process_and_validate_instruction_chain(
        &[
            (
                &system_instruction::transfer(&alice, &bob, 300_000),
                &[
                    Check::success(),
                    Check::account(&alice).lamports(700_000).build(),
                    Check::account(&bob).lamports(300_000).build(),
                ],
            ),
            (
                &system_instruction::transfer(&bob, &carol, 100_000),
                &[
                    Check::success(),
                    Check::account(&bob).lamports(200_000).build(),
                    Check::account(&carol).lamports(100_000).build(),
                ],
            ),
        ],
        &[
            (alice, system_account(starting_lamports)),
            (bob, system_account(0)),
            (carol, system_account(0)),
        ],
    );
}

Important: Instruction chains are NOT equivalent to Solana transactions. Mollusk doesn't impose transaction constraints like loaded account keys or size limits. Chains are primarily for testing program execution flows.

Time-Dependent Testing with warp_to_slot

Test logic that depends on clock or slot:

use solana_sdk::clock::Clock;

#[test]
fn test_time_dependent_logic() {
    let mut mollusk = Mollusk::default();

    // Warp to a specific slot
    mollusk.warp_to_slot(1000);

    // Test logic that depends on clock.slot
    let result1 = mollusk.process_instruction(&time_check_ix, &accounts);
    assert!(result1.is_ok());

    // Warp forward in time
    mollusk.warp_to_slot(2000);

    // Test again with new slot
    let result2 = mollusk.process_instruction(&time_check_ix, &accounts);
    assert!(result2.is_ok());
}

Custom Sysvar Configuration

Modify sysvars to test specific conditions:

use solana_sdk::rent::Rent;

#[test]
fn test_with_custom_rent() {
    let mut mollusk = Mollusk::default();

    // Customize rent parameters
    mollusk.sysvars.rent = Rent {
        lamports_per_byte_year: 1,
        exemption_threshold: 1.0,
        burn_percent: 0,
    };

    // Test with custom rent configuration
    let result = mollusk.process_instruction(&instruction, &accounts);
    assert!(result.is_ok());
}

Testing with Built-in Programs

Default builtins:

// Mollusk::default() includes subset of builtin programs
let mollusk = Mollusk::default();  // Includes System, BPF Loader, etc.

All builtins:

[dev-dependencies]
mollusk-svm = { version = "0.9", features = ["all-builtins"] }

Adding specific programs:

use mollusk_svm_programs_token::token;

let mut mollusk = Mollusk::default();
token::add_program(&mut mollusk);  // Add Token program

Anchor-Specific Testing

anchor test Command and Workflow

Anchor provides integrated testing via the anchor test command:

# Run all tests
anchor test

# Run tests without rebuilding
anchor test --skip-build

# Run tests without deploying (use existing deployment)
anchor test --skip-deploy

# Run specific test file
anchor test -- --test test_initialize

# Show program logs
anchor test -- --nocapture

Standard workflow:

  1. anchor build - Build program
  2. anchor test - Deploy to local validator and run TypeScript tests
  3. Test files run against deployed program
  4. Validator shuts down after tests complete

TypeScript Tests with @coral-xyz/anchor

Basic test structure:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { MyProgram } from "../target/types/my_program";
import { expect } from "chai";

describe("my-program", () => {
  // Configure the client to use the local cluster
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.MyProgram as Program<MyProgram>;

  it("Initializes the program", async () => {
    // Test implementation
  });
});

Setting Up Test Environment

describe("my-program", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.MyProgram as Program<MyProgram>;
  const wallet = provider.wallet as anchor.Wallet;

  // Generate keypairs
  const user = anchor.web3.Keypair.generate();
  const account = anchor.web3.Keypair.generate();

  before(async () => {
    // Airdrop SOL for testing
    const airdropSig = await provider.connection.requestAirdrop(
      user.publicKey,
      2 * anchor.web3.LAMPORTS_PER_SOL
    );
    await provider.connection.confirmTransaction(airdropSig);
  });

  it("runs test", async () => {
    // Test code
  });
});

Invoking Instructions

it("initializes account", async () => {
  const [pda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
    [Buffer.from("seed"), user.publicKey.toBuffer()],
    program.programId
  );

  const tx = await program.methods
    .initialize(bump)
    .accounts({
      user: user.publicKey,
      account: pda,
      systemProgram: anchor.web3.SystemProgram.programId,
    })
    .signers([user])
    .rpc();

  console.log("Transaction signature:", tx);
});

With custom transaction options:

const tx = await program.methods
  .initialize(bump)
  .accounts({ /* ... */ })
  .signers([user])
  .rpc({
    skipPreflight: false,
    commitment: "confirmed",
  });

Reading Account State

it("reads account data", async () => {
  // Fetch account data
  const accountData = await program.account.myAccount.fetch(accountPubkey);

  // Assert values
  expect(accountData.value).to.equal(42);
  expect(accountData.owner.toString()).to.equal(user.publicKey.toString());
});

// Fetch multiple accounts
const accounts = await program.account.myAccount.all();
console.log("Found accounts:", accounts.length);

// Fetch with filters
const filtered = await program.account.myAccount.all([
  {
    memcmp: {
      offset: 8,  // Skip discriminator
      bytes: user.publicKey.toBase58(),
    },
  },
]);

Event Listeners

it("listens for events", async () => {
  let eventReceived = false;

  // Set up event listener
  const listener = program.addEventListener(
    "MyEvent",
    (event, slot) => {
      console.log("Event received in slot:", slot);
      console.log("Event data:", event);
      eventReceived = true;
    }
  );

  // Trigger event
  await program.methods
    .triggerEvent()
    .accounts({ /* ... */ })
    .rpc();

  // Wait for event
  await new Promise((resolve) => setTimeout(resolve, 1000));

  expect(eventReceived).to.be.true;

  // Clean up listener
  await program.removeEventListener(listener);
});

LiteSVM for Fast Anchor Tests

LiteSVM provides a faster alternative to the full validator for Anchor tests:

Installation:

cargo add litesvm --dev

Basic usage:

use {
    litesvm::LiteSVM,
    solana_sdk::{
        message::Message,
        pubkey::Pubkey,
        signature::{Keypair, Signer},
        system_instruction::transfer,
        transaction::Transaction,
    },
};

#[test]
fn test_with_litesvm() {
    let from_keypair = Keypair::new();
    let from = from_keypair.pubkey();
    let to = Pubkey::new_unique();

    let mut svm = LiteSVM::new();
    svm.airdrop(&from, 10_000).unwrap();

    let instruction = transfer(&from, &to, 64);
    let tx = Transaction::new(
        &[&from_keypair],
        Message::new(&[instruction], Some(&from)),
        svm.latest_blockhash(),
    );
    let tx_res = svm.send_transaction(tx).unwrap();

    let from_account = svm.get_account(&from);
    let to_account = svm.get_account(&to);
    assert_eq!(from_account.unwrap().lamports, 4936);
    assert_eq!(to_account.unwrap().lamports, 64);
}

Deploying programs:

use solana_sdk::pubkey;

#[test]
fn test_program() {
    let program_id = pubkey!("Logging111111111111111111111111111111111111");
    let mut svm = LiteSVM::new();

    // Load program from file
    let bytes = include_bytes!("../target/deploy/my_program.so");
    svm.add_program(program_id, bytes);

    // Test program
    // ...
}

Time travel with LiteSVM:

use solana_sdk::clock::Clock;

#[test]
fn test_set_clock() {
    let mut svm = LiteSVM::new();

    // Get current clock
    let mut clock = svm.get_sysvar::<Clock>();

    // Set specific timestamp
    clock.unix_timestamp = 1735689600;  // January 1st 2025
    svm.set_sysvar::<Clock>(&clock);

    // Test time-dependent logic
    // ...

    // Warp to specific slot
    svm.warp_to_slot(1000);
}

Writing arbitrary accounts:

use {
    solana_sdk::account::Account,
    spl_token::state::Account as TokenAccount,
};

#[test]
fn test_with_token_account() {
    let mut svm = LiteSVM::new();

    let user = Pubkey::new_unique();
    let usdc_mint = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

    // Create fake USDC balance
    let token_account_data = /* serialize TokenAccount with balance */;

    svm.set_account(
        user,
        Account {
            lamports: 1_000_000,
            data: token_account_data,
            owner: spl_token::id(),
            executable: false,
            rent_epoch: 0,
        },
    );

    // Test with USDC balance
    // ...
}

Anchor.toml Test Configuration

Configure testing behavior in Anchor.toml:

[toolchain]
anchor_version = "0.30.1"

[features]
resolution = true
skip-lint = false

[programs.localnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

[test]
startup_wait = 5000  # Wait before running tests (ms)
shutdown_wait = 2000  # Wait before shutting down validator (ms)
upgradeable = false  # Deploy as upgradeable program

[test.validator]
url = "https://api.mainnet-beta.solana.com"  # Clone from mainnet
ledger = ".anchor/test-ledger"
bind_address = "0.0.0.0"

[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"  # Clone Metaplex

[[test.validator.clone]]
address = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"  # Clone Token program

[[test.validator.account]]
address = "..."  # Clone specific account
filename = "account.json"

Anchor Testing Best Practices

  1. Use anchor.workspace: Automatically loads program IDL
  2. Airdrop SOL in before() hooks: Set up test accounts before tests
  3. Use proper commitment levels: confirmed or finalized for reliability
  4. Test error conditions: Use .simulate() to test expected failures
  5. Clean up between tests: Reset account state or use fresh keypairs
  6. Use --skip-build during iteration: Speed up test runs
  7. Test with realistic data: Don't just test happy paths

Native Rust Testing

Cargo Test Setup

Native Rust programs use standard Rust testing with Mollusk:

Project structure:

my-program/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── processor.rs
│   └── instruction.rs
└── tests/
    ├── test_initialize.rs
    ├── test_update.rs
    └── test_close.rs

Cargo.toml configuration:

[package]
name = "my-program"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]

[dependencies]
solana-program = "2.1"

[dev-dependencies]
mollusk-svm = "0.9"
mollusk-svm-programs-token = "0.9"
solana-sdk = "2.1"

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

[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1

[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1

Mollusk with Native Programs

Basic test example:

// tests/test_initialize.rs
use {
    mollusk_svm::Mollusk,
    my_program::{instruction::initialize, ID},
    solana_sdk::{
        account::Account,
        instruction::{AccountMeta, Instruction},
        pubkey::Pubkey,
    },
};

#[test]
fn test_initialize() {
    let program_id = ID;
    let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");

    let user = Pubkey::new_unique();
    let account = Pubkey::new_unique();

    let instruction = Instruction {
        program_id,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(account, false),
            AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
        ],
        data: initialize().data,
    };

    let accounts = vec![
        (user, Account {
            lamports: 10_000_000,
            data: vec![],
            owner: solana_sdk::system_program::id(),
            executable: false,
            rent_epoch: 0,
        }),
        (account, Account {
            lamports: 0,
            data: vec![],
            owner: solana_sdk::system_program::id(),
            executable: false,
            rent_epoch: 0,
        }),
    ];

    let result = mollusk.process_instruction(&instruction, &accounts);
    assert!(result.is_ok());
}

Manual Account Setup

Native Rust tests require explicit account setup:

use solana_sdk::account::Account;

// Helper: Create system account
fn system_account(lamports: u64) -> Account {
    Account {
        lamports,
        data: vec![],
        owner: solana_sdk::system_program::id(),
        executable: false,
        rent_epoch: 0,
    }
}

// Helper: Create program-owned account
fn program_account(lamports: u64, data: Vec<u8>, owner: Pubkey) -> Account {
    Account {
        lamports,
        data,
        owner,
        executable: false,
        rent_epoch: 0,
    }
}

// Helper: Create rent-exempt account
fn rent_exempt_account(data_len: usize, owner: Pubkey, mollusk: &Mollusk) -> Account {
    let lamports = mollusk.sysvars.rent.minimum_balance(data_len);
    Account {
        lamports,
        data: vec![0; data_len],
        owner,
        executable: false,
        rent_epoch: 0,
    }
}

// Usage
#[test]
fn test_with_helpers() {
    let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");

    let user = Pubkey::new_unique();
    let data_account = Pubkey::new_unique();

    let accounts = vec![
        (user, system_account(10_000_000)),
        (data_account, rent_exempt_account(100, program_id, &mollusk)),
    ];

    // Test
    // ...
}

Testing CPIs

Use mollusk-svm-programs-token for testing cross-program invocations:

use {
    mollusk_svm::{result::Check, Mollusk},
    mollusk_svm_programs_token::token,
    solana_sdk::{
        account::Account,
        program_pack::Pack,
        pubkey::Pubkey,
    },
    spl_token::state::{Account as TokenAccount, AccountState, Mint},
};

#[test]
fn test_token_transfer_cpi() {
    // Initialize Mollusk with Token program
    let mut mollusk = Mollusk::default();
    token::add_program(&mut mollusk);

    // Setup mint
    let mint = Pubkey::new_unique();
    let decimals = 6;

    let mut mint_data = vec![0u8; Mint::LEN];
    Mint::pack(
        Mint {
            mint_authority: Some(authority).into(),
            supply: 1_000_000,
            decimals,
            is_initialized: true,
            freeze_authority: None.into(),
        },
        &mut mint_data,
    ).unwrap();

    // Setup source token account
    let source = Pubkey::new_unique();
    let mut source_data = vec![0u8; TokenAccount::LEN];
    TokenAccount::pack(
        TokenAccount {
            mint,
            owner: authority,
            amount: 1_000_000,
            delegate: None.into(),
            state: AccountState::Initialized,
            is_native: None.into(),
            delegated_amount: 0,
            close_authority: None.into(),
        },
        &mut source_data,
    ).unwrap();

    // Setup destination token account
    let destination = Pubkey::new_unique();
    let mut dest_data = vec![0u8; TokenAccount::LEN];
    TokenAccount::pack(
        TokenAccount {
            mint,
            owner: recipient,
            amount: 0,
            delegate: None.into(),
            state: AccountState::Initialized,
            is_native: None.into(),
            delegated_amount: 0,
            close_authority: None.into(),
        },
        &mut dest_data,
    ).unwrap();

    let mint_rent = mollusk.sysvars.rent.minimum_balance(Mint::LEN);
    let account_rent = mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN);

    let accounts = vec![
        (source, Account {
            lamports: account_rent,
            data: source_data,
            owner: token::ID,
            executable: false,
            rent_epoch: 0,
        }),
        (mint, Account {
            lamports: mint_rent,
            data: mint_data,
            owner: token::ID,
            executable: false,
            rent_epoch: 0,
        }),
        (destination, Account {
            lamports: account_rent,
            data: dest_data,
            owner: token::ID,
            executable: false,
            rent_epoch: 0,
        }),
    ];

    // Create transfer instruction
    use spl_token::instruction::transfer_checked;

    let instruction = transfer_checked(
        &token::ID,
        &source,
        &mint,
        &destination,
        &authority,
        &[],
        500_000,
        decimals,
    ).unwrap();

    // Validate transfer
    let checks = vec![
        Check::success(),
        Check::account(&source)
            .data_slice(64, &(500_000u64).to_le_bytes())
            .build(),
        Check::account(&destination)
            .data_slice(64, &(500_000u64).to_le_bytes())
            .build(),
    ];

    mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}

Validation Patterns

Account state validation:

use mollusk_svm::result::Check;

let checks = vec![
    Check::success(),
    Check::account(&account_pubkey)
        .lamports(expected_lamports)
        .data(&expected_data)
        .owner(&expected_owner)
        .build(),
];

mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);

Error validation:

use solana_sdk::instruction::InstructionError;

let checks = vec![
    Check::instruction_err(InstructionError::InvalidAccountData),
];

mollusk.process_and_validate_instruction(&bad_instruction, &accounts, &checks);

Compute unit validation:

let checks = vec![
    Check::success(),
    Check::compute_units(5000),  // Exactly 5000 CU
];

Data slice validation:

// Check specific bytes without loading full account data
let checks = vec![
    Check::account(&account)
        .data_slice(8, &[1, 2, 3, 4])  // Check bytes 8-11
        .build(),
];

Next Steps