Mollusk
Mollusk는 Solana 프로그램을 테스트하기 위한 경량 테스트 하네스입니다. 간소화된 Solana 가상 머신(SVM) 환경에서 Solana 프로그램 명령어를 테스트하기 위한 간단한 인터페이스를 제공합니다. 모든 테스트 계정은 명시적으로 정의되어야 하므로 결정적이고 반복 가능한 테스트를 보장합니다.
설치
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"
Mollusk로 테스트할 때 Token Program, Token2022 Program(Token Extensions) 및
Associated Token Program을 사용하려면 mollusk-svm-programs-token
를
Cargo.toml
에 의존성으로 추가하세요:
$cargo add mollusk-svm-programs-token --dev
[dev-dependencies]mollusk-svm-programs-token = "0.5"
Mollusk SVM
다음 예제는 Mollusk를 사용하여 기본 Solana 프로그램을 테스트하기 위한 최소한의 설정을 보여줍니다.
Hello World 프로그램
이 예제는 Mollusk를 사용하여 기본 Solana 프로그램을 테스트하는 방법을 보여줍니다. 이 프로그램은 호출될 때 프로그램 로그에 "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()]);}}
Mollusk로 Solana 프로그램을 테스트하려면:
Mollusk
인스턴스 생성 - 프로그램 ID와 컴파일된 프로그램 경로(.so
파일)로 Mollusk 초기화- 명령어 구성 - 프로그램을 호출하기 위한 명령어 생성
- 처리 및 검증 - 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 | 명령어를 처리하고 결과에 대해 일련의 검사를 수행하며, 검사가 실패하면 패닉이 발생합니다. |
process_instruction_chain | 여러 명령어를 처리하고 결과를 반환합니다. |
process_and_validate_instruction_chain | 여러 명령어를 처리하고 각 결과에 대해 일련의 검사를 수행하며, 검사가 실패하면 패닉이 발생합니다. |
InstructionResult
에는
처리된 명령어의 세부 정보가 포함되어 있습니다.
단일 명령어
결과에 대한 검사 없이 단일 명령어를 처리하려면 process_instruction
메서드를
사용하세요. 처리 후 수동으로 결과를 검증할 수 있습니다.
pub fn process_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],) -> InstructionResult
다음 예제는 검증 검사 없이 SOL 전송 명령어를 처리합니다.
아래 예제는 시연 목적으로 main
함수에서 Mollusk를 실행합니다. 실제로는
일반적으로 #[test]
속성으로 주석이 달린 테스트 모듈에서 Mollusk를 사용하게
됩니다.
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
메서드를 사용하여 검증 확인이 포함된 단일
명령어를 처리합니다. 이 메서드는 검증에 실패하면 패닉을 발생시킵니다.
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
와 동일합니다.
각 메소드에 accounts
를 전달해야 하는 Mollusk
(예: process_instruction
)와
달리, MolluskContext
는 account_store
를 통해 내부적으로 계정을 관리합니다.
이로 인해 명령어를 처리할 때 accounts
매개변수가 필요하지 않습니다.
with_context
메소드를 사용하여 account_store
를 생성하세요:
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);
다음 예제는 account_store
를 통해 명령어 간에 영구적인 계정 상태를 유지하면서
두 개의 별도 SOL 전송 명령어를 처리합니다.
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);}
다음 예제는 sysvars
필드에 접근하여 rent 매개변수를 변경하기 위해 Mollusk
시스템 변수를 직접 수정하는 방법을 보여줍니다. 다른 시스템 변수 값도 같은
방식으로 수정할 수 있습니다.
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
프로그램 명령어의 컴퓨트 유닛 사용량을 추적합니다. 결과는 마크다운 파일로
작성됩니다.
의존성으로
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
에 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
에는 민트와 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?