Mollusk
Mollusk — це легка тестова оболонка для тестування програм Solana. Вона надає простий інтерфейс для тестування інструкцій програм Solana у мінімізованому середовищі Solana Virtual Machine (SVM). Усі тестові облікові записи мають бути явно визначені, що забезпечує детерміновані та повторювані тести.
Встановлення
Додайте mollusk-svm
як залежність у Cargo.toml
:
$cargo add mollusk-svm --dev
[dev-dependencies]mollusk-svm = "0.5"
Для оцінки використання обчислювальних одиниць додайте mollusk-svm-bencher
як
залежність у Cargo.toml
:
$cargo add mollusk-svm-bencher --dev
[dev-dependencies]mollusk-svm-bencher = "0.5"
Щоб використовувати Token Program, Token2022 Program (Token Extensions) та
Associated Token Program для тестування з Mollusk, додайте
mollusk-svm-programs-token
як залежність у Cargo.toml
:
$cargo add mollusk-svm-programs-token --dev
[dev-dependencies]mollusk-svm-programs-token = "0.5"
Mollusk SVM
Наступний приклад показує мінімальну конфігурацію для тестування базової програми Solana за допомогою Mollusk.
Програма Hello World
Цей приклад демонструє, як тестувати базову програму Solana за допомогою Mollusk. Програма просто виводить "Hello, world!" у журнал програми при виклику.
Запуск cargo build-sbf
генерує скомпільовану програму в
/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()]);}}
Для тестування програми Solana з Mollusk:
- Створіть екземпляр
Mollusk
- Ініціалізуйте Mollusk з ідентифікатором програми та шляхом до скомпільованої програми (файл.so
) - Створіть інструкцію - Створіть інструкцію для виклику програми
- Обробіть та перевірте - Обробіть інструкцію за допомогою Mollusk та перевірте результат
#[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()]);}}
Щоб виконати тест, запустіть cargo test
.
Коли тест виконається успішно, ви побачите вивід, схожий на наступний:
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
Структура
Mollusk
надає простий інтерфейс для тестування програм Solana. Усіма полями можна
керувати через кілька допоміжних методів, але користувачі також можуть
безпосередньо отримувати доступ та змінювати їх, якщо бажають більшого контролю.
Щоб ініціалізувати Mollusk з екземпляром за замовчуванням, використовуйте метод
Mollusk::default
.
// Default instance with no custom programslet mollusk = Mollusk::default();
Щоб ініціалізувати Mollusk з конкретною програмою, використовуйте метод
Mollusk::new
.
// 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");
Щоб додати програму до Mollusk, використовуйте метод Mollusk::add_program
.
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(),);
Коли вказуєте шлях до файлу, не включайте розширення .so
. Наприклад,
"path/to/my_program"
правильно, а "path/to/my_program.so"
неправильно.
Обробка інструкцій
Mollusk надає чотири основні методи для обробки інструкцій:
Метод | Опис |
---|---|
process_instruction | Обробляє інструкцію та повертає результат. |
process_and_validate_instruction | Обробляє інструкцію та виконує серію перевірок результату, викликаючи panic, якщо будь-яка перевірка не пройдена. |
process_instruction_chain | Обробляє кілька інструкцій та повертає результат. |
process_and_validate_instruction_chain | Обробляє кілька інструкцій та виконує серію перевірок для кожного результату, викликаючи panic, якщо будь-яка перевірка не пройдена. |
InstructionResult
містить деталі обробленої інструкції.
Одиночна інструкція
Використовуйте метод process_instruction
для обробки однієї інструкції без
перевірок результату. Ви можете вручну перевірити результати після обробки.
pub fn process_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],) -> InstructionResult
Наступний приклад обробляє інструкцію переказу SOL без перевірок валідації.
Приклади нижче запускають Mollusk у функції main
для демонстраційних цілей. На
практиці ви зазвичай використовуватимете Mollusk у тестовому модулі з атрибутом
#[test]
.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};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 = system_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);}
Одна інструкція з перевірками
Використовуйте метод process_and_validate_instruction
для обробки однієї
інструкції з перевірками валідації. Цей метод викличе panic, якщо будь-яка
перевірка не пройде.
pub fn process_and_validate_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],checks: &[Check],) -> InstructionResult
Наступний приклад обробляє інструкцію переказу SOL з перевірками валідації.
use {mollusk_svm::{Mollusk, result::Check},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};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 = system_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);}
Кілька інструкцій
Використовуйте метод process_instruction_chain
для послідовної обробки кількох
інструкцій без перевірок валідації.
pub fn process_instruction_chain(&self,instructions: &[Instruction],accounts: &[(Pubkey, Account)],) -> InstructionResult
Наступний приклад обробляє дві інструкції переказу SOL без перевірок валідації.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};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![system_instruction::transfer(&alice, &bob, 300_000), // Alice -> Bobsystem_instruction::transfer(&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);}
Кілька інструкцій з перевірками
Використовуйте метод process_and_validate_instruction_chain
для обробки
кількох інструкцій з перевірками валідації після кожної інструкції. Кожна
інструкція має свій власний набір перевірок, які повинні пройти.
pub fn process_and_validate_instruction_chain(&self,instructions: &[(&Instruction, &[Check])],accounts: &[(Pubkey, Account)],) -> InstructionResult
Наступний приклад обробляє послідовність із двох інструкцій переказу SOL з перевірками валідації після кожної інструкції.
use {mollusk_svm::{result::Check, Mollusk},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};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 = system_instruction::transfer(&alice, &bob, 300_000);let transfer2 = system_instruction::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);}
Перевірки валідації
Mollusk надає набір допоміжних методів для перевірки результатів обробленої інструкції.
use mollusk_svm::result::Check;
Використовуйте такі методи для перевірки результатів інструкцій:
// 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])
Використовуйте такі методи для перевірки станів облікових записів:
// 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()
Постійний стан облікового запису
MolluskContext
є обгорткою навколо Mollusk
, яка підтримує стан облікового запису під час
кількох викликів інструкцій через свій account_store
. Методи обробки
інструкцій ідентичні до Mollusk
.
На відміну від Mollusk
, який вимагає передачі accounts
до кожного методу
(наприклад, process_instruction
), MolluskContext
керує обліковими записами
внутрішньо через свій account_store
. Це усуває потребу в параметрі accounts
під час обробки інструкцій.
Створіть account_store
за допомогою методу with_context
:
use std::collections::HashMap;use solana_sdk::{account::Account, pubkey::Pubkey, system_program};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);
Наступний приклад обробляє дві окремі інструкції переказу SOL з постійним станом
облікового запису між інструкціями через account_store
.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},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 = system_instruction::transfer(&sender, &recipient, 200_000);context.process_instruction(&instruction1);// Second transfer: 100,000 lamports (state persists from first transfer)let instruction2 = system_instruction::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
Mollusk надає власну структуру
Sysvars
для зміни її значень для тестування.
Використовуйте метод warp_to_slot
для оновлення системного годинника, щоб
імітувати рух вперед або назад у часі до конкретного 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);}
Наступний приклад показує, як змінювати системну змінну Mollusk безпосередньо
через доступ до поля sysvars
для зміни параметрів rent. Ви можете змінювати
інші значення системних змінних таким самим способом.
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));}
Тестування обчислювальних одиниць
MolluskComputeUnitBencher
відстежує використання обчислювальних одиниць інструкціями програми. Результати
записуються у файл markdown.
Потребує
mollusk-svm-bencher
як залежність.
Наступний приклад тестує використання обчислювальних одиниць інструкцією переказу SOL.
use {mollusk_svm::Mollusk,mollusk_svm_bencher::MolluskComputeUnitBencher,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create test accountslet sender = Pubkey::new_unique();let receiver = Pubkey::new_unique();// Transfer instructionlet transfer = system_instruction::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();}
Результати тестування записуються у вказаний out_dir
як файл markdown з назвою
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
Використовуйте
mollusk-svm-programs-token
пакет для додавання token program, token2022 program (token extensions) та
associated token program до Mollusk для тестування.
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);
Наступний приклад демонструє тестування переказу токенів за допомогою Mollusk.
Приклад нижче вручну визначає тестові облікові записи для демонстраційних
цілей. mollusk-svm-programs-token
також містить допоміжні функції для
створення mint та token account.
use {mollusk_svm::{result::Check, Mollusk},mollusk_svm_programs_token::token,solana_sdk::{account::Account, program_pack::Pack, pubkey::Pubkey},spl_token::{instruction::transfer_checked,state::{Account as TokenAccount, 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: spl_token::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: spl_token::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?