Mollusk is een lichtgewicht test framework voor het testen van Solana-programma's. Het biedt een eenvoudige interface voor het testen van Solana-programma-instructies in een geminimaliseerde Solana Virtual Machine (SVM) omgeving. Alle testaccounts moeten expliciet worden gedefinieerd, wat zorgt voor deterministische en herhaalbare tests.
Installatie
Voeg mollusk-svm toe als dependency in Cargo.toml:
$cargo add mollusk-svm --dev
[dev-dependencies]mollusk-svm = "0.7"
Om het gebruik van compute units te benchmarken, voeg mollusk-svm-bencher toe
als dependency in Cargo.toml:
$cargo add mollusk-svm-bencher --dev
[dev-dependencies]mollusk-svm-bencher = "0.7"
Om het Token Program, token2022 programma (Token Extensions) en Associated Token
Program te gebruiken voor testen met Mollusk, voeg mollusk-svm-programs-token
toe als dependency in Cargo.toml:
$cargo add mollusk-svm-programs-token --dev
[dev-dependencies]mollusk-svm-programs-token = "0.7"
Mollusk SVM
Het volgende voorbeeld toont een minimale setup voor het testen van een eenvoudig Solana-programma met Mollusk.
Hello World-programma
Dit voorbeeld laat zien hoe je een eenvoudig Solana-programma kunt testen met Mollusk. Het programma print simpelweg "Hello, world!" naar de programmalogs wanneer het wordt aangeroepen.
Het uitvoeren van cargo build-sbf genereert het gecompileerde programma op
/target/deploy/<program_name>.so.
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(_program_id: &Pubkey,_accounts: &[AccountInfo],_instruction_data: &[u8],) -> ProgramResult {msg!("Hello, world!");Ok(())}#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
Om een Solana-programma te testen met Mollusk:
- Maak een
Molluskinstantie - Initialiseer Mollusk met een programma-ID en het pad naar het gecompileerde programma (.sobestand) - Bouw een instructie - Maak een instructie om het programma aan te roepen
- Verwerk en valideer - Verwerk de instructie met behulp van Mollusk en valideer het resultaat
#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
Om de test uit te voeren, voer cargo test uit.
Wanneer de test succesvol wordt uitgevoerd, zie je uitvoer vergelijkbaar met het volgende:
running 1 test[2025-09-22T19:25:50.427685000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs invoke [1][2025-09-22T19:25:50.429669000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world![2025-09-22T19:25:50.429690000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs consumed 211 of 1400000 compute units[2025-09-22T19:25:50.429726000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs successtest tests::test_hello_world ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02sDoc-tests hello_world
De
Mollusk
struct biedt een eenvoudige interface voor het testen van Solana-programma's.
Alle velden kunnen worden gemanipuleerd via een handvol hulpmethoden, maar
gebruikers kunnen ze ook direct openen en wijzigen als ze meer controle willen.
Om Mollusk te initialiseren met een standaard instantie gebruik je de
Mollusk::default methode.
// Default instance with no custom programslet mollusk = Mollusk::default();
Om Mollusk te initialiseren met een specifiek programma gebruik je de
Mollusk::new methode.
// Initialize Mollusk with a specific program from a file pathlet program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
Om een programma toe te voegen aan Mollusk gebruik je de Mollusk::add_program
methode.
let mollusk = Mollusk::default();let program_id = Pubkey::new_unique();// Add a program to Molluskmollusk.add_program(&program_id,"target/deploy/my_program",&bpf_loader_upgradeable::id(),);
Wanneer je het bestandspad opgeeft, voeg dan niet de .so extensie toe.
Bijvoorbeeld, "path/to/my_program" is correct, maar
"path/to/my_program.so" is niet correct.
Instructies verwerken
Mollusk biedt vier hoofdmethoden voor het verwerken van instructies:
| Methode | Beschrijving |
|---|---|
process_instruction | Verwerk een instructie en retourneer het resultaat. |
process_and_validate_instruction | Verwerk een instructie en voer een reeks controles uit op het resultaat, met paniek als een controle faalt. |
process_instruction_chain | Verwerk meerdere instructies en retourneer het resultaat. |
process_and_validate_instruction_chain | Verwerk meerdere instructies en voer een reeks controles uit op elk resultaat, met paniek als een controle faalt. |
Het
InstructionResult
bevat de details van een verwerkte instructie.
Enkele instructie
Gebruik de process_instruction methode om een enkele instructie te verwerken
zonder controles op het resultaat. Je kunt de resultaten handmatig valideren na
verwerking.
pub fn process_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],) -> InstructionResult
Het volgende voorbeeld verwerkt een SOL overdrachts-instructie zonder validatie controles.
De onderstaande voorbeelden draaien Mollusk in de main functie ter
demonstratiedoeleinden. In de praktijk gebruik je Mollusk doorgaans in een
testmodule geannoteerd met het #[test] attribuut.
use mollusk_svm::Mollusk;use solana_sdk::{account::Account, pubkey::Pubkey};use solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Set up accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;// Create transfer instructionlet instruction = transfer(&sender, &recipient, transfer_amount);// Define initial account stateslet accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),];// Process the instructionlet result = mollusk.process_instruction(&instruction, &accounts);println!("{:#?}", result);// Check the resultassert!(result.program_result.is_ok());assert_eq!(result.get_account(&sender).unwrap().lamports, 750_000);assert_eq!(result.get_account(&recipient).unwrap().lamports, 250_000);}
Enkele instructie met controles
Gebruik de process_and_validate_instruction methode om een enkele instructie
te verwerken met validatiecontroles. Deze methode zal in paniek raken als een
controle faalt.
pub fn process_and_validate_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],checks: &[Check],) -> InstructionResult
Het volgende voorbeeld verwerkt een SOL-overdrachtsinstructie met validatiecontroles.
use {mollusk_svm::{result::Check, Mollusk},solana_sdk::{account::Account, pubkey::Pubkey},solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID},};fn main() {let mollusk = Mollusk::default();let sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;let instruction = transfer(&sender, &recipient, transfer_amount);let accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),];// Define validation checkslet checks = vec![Check::success(),Check::account(&sender).lamports(750_000).build(),Check::account(&recipient).lamports(250_000).build(),];// Process and validate (will panic if any check fails)let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);}
Meerdere instructies
Gebruik de process_instruction_chain methode om meerdere instructies
sequentieel te verwerken zonder validatiecontroles.
pub fn process_instruction_chain(&self,instructions: &[Instruction],accounts: &[(Pubkey, Account)],) -> InstructionResult
Het volgende voorbeeld verwerkt twee SOL-overdrachtsinstructies zonder validatiecontroles.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey},solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID},};fn main() {let mollusk = Mollusk::default();// Set up accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create chain of transferslet instructions = vec![transfer(&alice, &bob, 300_000), // Alice -> Bobtransfer(&bob, &charlie, 100_000), // Bob -> Charlie];let accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),];// Process the instruction chainlet result = mollusk.process_instruction_chain(&instructions, &accounts);println!("{:#?}", result);// Final balances: Alice=700K, Bob=200K, Charlie=100Kassert_eq!(result.get_account(&alice).unwrap().lamports, 700_000);assert_eq!(result.get_account(&bob).unwrap().lamports, 200_000);assert_eq!(result.get_account(&charlie).unwrap().lamports, 100_000);}
Meerdere instructies met controles
Gebruik de process_and_validate_instruction_chain methode om meerdere
instructies te verwerken met validatiecontroles na elke instructie. Elke
instructie heeft zijn eigen set controles die moeten slagen.
pub fn process_and_validate_instruction_chain(&self,instructions: &[(&Instruction, &[Check])],accounts: &[(Pubkey, Account)],) -> InstructionResult
Het volgende voorbeeld verwerkt een reeks van twee SOL-overdrachtsinstructies met validatiecontroles na elke instructie.
use {mollusk_svm::{result::Check, Mollusk},solana_sdk::{account::Account, pubkey::Pubkey},solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID},};fn main() {let mollusk = Mollusk::default();// Create accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create transfer instructionslet transfer1 = transfer(&alice, &bob, 300_000);let transfer2 = transfer(&bob, &charlie, 100_000);// Initial accountslet accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),];// Define checks for each instructionlet checks_after_transfer1 = vec![Check::success(),Check::account(&alice).lamports(700_000) // 1M - 300K.build(),Check::account(&bob).lamports(300_000) // 0 + 300K.build(),Check::account(&charlie).lamports(0) // Unchanged.build(),];let checks_after_transfer2 = vec![Check::success(),Check::account(&alice).lamports(700_000) // Unchanged from previous.build(),Check::account(&bob).lamports(200_000) // 300K - 100K.build(),Check::account(&charlie).lamports(100_000) // 0 + 100K.build(),];// Process with validation at each steplet instruction_and_checks = [(&transfer1, checks_after_transfer1.as_slice()),(&transfer2, checks_after_transfer2.as_slice()),];// Execute chain (panics if any check fails)let result = mollusk.process_and_validate_instruction_chain(&instruction_and_checks, &accounts);println!("{:#?}", result);}
Validatiecontroles
Mollusk biedt een reeks hulpmethoden om de resultaten van een verwerkte instructie te controleren.
use mollusk_svm::result::Check;
Gebruik de volgende methoden om instructieresultaten te valideren:
// Program execution succeededCheck::success()// Program returned specific errorCheck::err(ProgramError::InvalidArgument)// Instruction level errorCheck::instruction_err(InstructionError::InsufficientFunds)// Check with specific program resultCheck::program_result(ProgramResult::Success)// Compute units consumedCheck::compute_units(1000)// Execution timeCheck::time(100)// Return data from instruction executionCheck::return_data(&[1, 2, 3, 4])
Gebruik het volgende om accountstatussen te valideren:
// Single account validationCheck::account(&pubkey).lamports(1_000_000) // Exact lamports.owner(&program_id) // Account owner.data(&expected_data) // Exact data match.data_slice(8, &[1, 2, 3]) // Partial data match at offset.executable(false) // Executable flag.space(100) // Account data size.closed() // Account is closed (0 lamports).rent_exempt() // Account is rent-exempt.build()// Check all accounts are rent exemptCheck::all_rent_exempt()
Permanente accountstatus
De
MolluskContext
is een wrapper rond Mollusk die accountstatus behoudt over meerdere
instructie-aanroepen heen via zijn account_store. De methoden voor het
verwerken van instructies zijn identiek aan Mollusk.
In tegenstelling tot Mollusk, dat vereist dat accounts aan elke methode
wordt doorgegeven (bijv. process_instruction), beheert MolluskContext
accounts intern via zijn account_store. Dit elimineert de noodzaak voor de
accounts-parameter bij het verwerken van instructies.
Maak een account_store aan met de with_context-methode:
use std::collections::HashMap;use solana_sdk::{account::Account, pubkey::Pubkey};use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;use mollusk_svm::Mollusk;let mollusk = Mollusk::default();let account_address = Pubkey::new_unique();let mut account_store = HashMap::new();account_store.insert(account_address,Account {lamports: 1_000_000,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},);let context = mollusk.with_context(account_store);
Het volgende voorbeeld verwerkt twee afzonderlijke SOL-overdrachtsinstructies
met persistente accountstatus tussen de instructies via de account_store.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey},solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID},std::collections::HashMap,};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();// Create account store with initial balanceslet mut account_store = HashMap::new();account_store.insert(sender,Account {lamports: 1_000_000,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},);account_store.insert(recipient,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},);// Create a stateful contextlet context = mollusk.with_context(account_store);// First transfer: 200,000 lamportslet instruction1 = transfer(&sender, &recipient, 200_000);context.process_instruction(&instruction1);// Second transfer: 100,000 lamports (state persists from first transfer)let instruction2 = transfer(&sender, &recipient, 100_000);context.process_instruction(&instruction2);// Check final balanceslet store = context.account_store.borrow();let sender_account = store.get(&sender).unwrap();let recipient_account = store.get(&recipient).unwrap();println!("Sender: {:#?}", sender_account);println!("Recipient: {:#?}", recipient_account);}
Mollusk Sysvars
Mollusk biedt een aangepaste
Sysvars-struct
voor het aanpassen van waarden voor testen.
Gebruik de warp_to_slot-methode om de sysvar-klok bij te werken om vooruit of
achteruit in de tijd te simuleren naar een specifiek slot.
use mollusk_svm::Mollusk;fn main() {// Initialize Mollusklet mut mollusk = Mollusk::default();// Show initial slotprintln!("Initial slot: {}", mollusk.sysvars.clock.slot);// Warp to slot 1000mollusk.warp_to_slot(100);println!("After warp: {}", mollusk.sysvars.clock.slot);// Warp to slot 10mollusk.warp_to_slot(10);println!("After second warp: {}", mollusk.sysvars.clock.slot);}
Het volgende voorbeeld toont hoe je de Mollusk-sysvar direct kunt aanpassen door
toegang te krijgen tot het sysvars-veld om de rent-parameters te wijzigen. Je
kunt andere sysvar-waarden op dezelfde manier aanpassen.
use {mollusk_svm::Mollusk, solana_sdk::rent::Rent};fn main() {let mut mollusk = Mollusk::default();// Show default rentprintln!("Default rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));// Customize rent parametersmollusk.sysvars.rent = Rent {lamports_per_byte_year: 1,exemption_threshold: 1.0,burn_percent: 0,};// Show custom rentprintln!("Custom rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));}
Compute unit benchmarking
MolluskComputeUnitBencher
volgt het gebruik van compute units van de instructies van een programma. De
resultaten worden naar een markdown-bestand geschreven.
Vereist
mollusk-svm-bencher
als afhankelijkheid.
Het volgende voorbeeld benchmarkt het compute unit gebruik van een SOL transfer instructie.
use {mollusk_svm::Mollusk,mollusk_svm_bencher::MolluskComputeUnitBencher,solana_sdk::{account::Account, pubkey::Pubkey},solana_system_interface::{instruction::transfer, program::ID as SYSTEM_PROGRAM_ID},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create test accountslet sender = Pubkey::new_unique();let receiver = Pubkey::new_unique();// Transfer instructionlet transfer = transfer(&sender, &receiver, 100_000);let accounts = vec![(sender,Account {lamports: 1_000_000,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),(receiver,Account {lamports: 0,data: vec![],owner: SYSTEM_PROGRAM_ID,executable: false,rent_epoch: 0,},),];// Run benchmarkMolluskComputeUnitBencher::new(mollusk).bench(("transfer", &transfer, &accounts)).must_pass(true).out_dir("./target/benches").execute();}
De benchmarkresultaten worden naar de opgegeven out_dir geschreven als een
markdown-bestand met de naam compute_units.md.
#### 2025-09-19 22:28:53.691839 UTCSolana CLI Version: solana-cli 2.2.20 (src:dabc99a5; feat:3073396398,client:Agave)| Name | CUs | Delta || -------- | --- | ------- || transfer | 150 | - new - |
Token Program testen
Gebruik de
mollusk-svm-programs-token-crate
om het Token Program, Token2022 Program (Token Extensions) en Associated Token
Program toe te voegen aan Mollusk voor testen.
use {mollusk_svm::Mollusk,mollusk_svm_programs_token::{associated_token, token, token2022},};let mut mollusk = Mollusk::default();// Add SPL Token Programtoken::add_program(&mut mollusk);// Add SPL Token-2022 Programtoken2022::add_program(&mut mollusk);// Add Associated Token Account Programassociated_token::add_program(&mut mollusk);
Het volgende voorbeeld demonstreert het testen van een token-overdracht met Mollusk.
Het onderstaande voorbeeld definieert de testaccounts handmatig ter
demonstratie. De mollusk-svm-programs-token bevat ook hulpfuncties om de
mint- en tokenaccounts aan te maken.
use {mollusk_svm::{result::Check, Mollusk},mollusk_svm_programs_token::token,solana_sdk::{account::Account, program_pack::Pack, pubkey::Pubkey},spl_token_interface::{instruction::transfer_checked,state::{Account as TokenAccount, AccountState, Mint},},};fn main() {// Initialize Mollusk with Token programlet mut mollusk = Mollusk::default();token::add_program(&mut mollusk);// Create account keyslet mint = Pubkey::new_unique();let source = Pubkey::new_unique();let destination = Pubkey::new_unique();let authority = Pubkey::new_unique();// Token configurationlet decimals = 6;let transfer_amount = 1_000_000; // 1 token with 6 decimalslet initial_balance = 10_000_000; // 10 tokens// Calculate rent-exempt minimumslet mint_rent = mollusk.sysvars.rent.minimum_balance(Mint::LEN);let account_rent = mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN);// Create mint accountlet mut mint_data = vec![0u8; Mint::LEN];Mint::pack(Mint {mint_authority: Some(authority).into(),supply: initial_balance,decimals,is_initialized: true,freeze_authority: None.into(),},&mut mint_data,).unwrap();// Create source token accountlet mut source_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: authority,amount: initial_balance,delegate: None.into(),state: AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut source_data,).unwrap();// Create destination token accountlet mut destination_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: Pubkey::new_unique(),amount: 0,delegate: None.into(),state: AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut destination_data,).unwrap();// Setup accounts for transfer_checkedlet 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: destination_data,owner: token::ID,executable: false,rent_epoch: 0,},),(authority,Account {lamports: 1_000_000,data: vec![],owner: Pubkey::default(),executable: false,rent_epoch: 0,},),];// Create transfer_checked instructionlet instruction = transfer_checked(&token::ID,&source,&mint,&destination,&authority,&[],transfer_amount,decimals,).unwrap();// Expected balances after transferlet expected_source_balance = (initial_balance - transfer_amount).to_le_bytes();let expected_dest_balance = transfer_amount.to_le_bytes();// Define validation checkslet checks = vec![Check::success(),Check::account(&source).data_slice(64, &expected_source_balance) // Token amount is at offset 64.build(),Check::account(&destination).data_slice(64, &expected_dest_balance).build(),];// Process and validate the instructionlet result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);// Deserialize token account datalet source_account = result.get_account(&source).unwrap();let source_token = TokenAccount::unpack(&source_account.data).unwrap();println!("Source Token Account: {:#?}", source_token);let destination_account = result.get_account(&destination).unwrap();let dest_token = TokenAccount::unpack(&destination_account.data).unwrap();println!("Destination Token Account: {:#?}", dest_token);}
Is this page helpful?