高效测试 Solana 程序需要一个能够平衡速度、精确性和洞察力的框架。在开发复杂的程序逻辑时,您需要一个既能快速迭代又不牺牲测试边界情况或准确测量性能能力的环境。
理想的 Solana 测试框架应具备以下三个基本功能:
Mollusk 通过提供一个专为 Solana 程序开发设计的精简测试环境,满足了这些需求。
Mollusk 是由 Anza 团队的 Joe Caulfield 创建并维护的一个轻量级 Solana 程序测试工具,它提供了一个直接的程序执行接口,而无需完整 validator 运行时的开销。
Mollusk 并未模拟完整的 validator 环境,而是使用低级 Solana 虚拟机 (SVM) 组件构建了一个程序执行管道。这种方法在保留全面程序测试所需的基本功能的同时,消除了不必要的开销。
该框架通过排除 Agave validator 实现中的 AccountsDB 和 Bank 等重量级组件,实现了卓越的性能。这种设计选择需要显式的账户配置,但实际上成为了一种优势,因为它赋予了对账户状态的精确控制,并能够测试在完整 validator 环境中难以重现的场景。
Mollusk 的测试工具支持全面的配置选项,包括计算预算调整、功能集修改和 sysvar 自定义。这些配置通过 Mollusk 结构直接管理,并可使用内置的辅助函数进行修改。
核心 mollusk-svm crate 提供了基础的测试基础设施,而其他的 crate 提供了针对常见 Solana 程序(如 Token 和 Memo 程序)的专用辅助工具。
将主 Mollusk crate 添加到您的项目中:
cargo add mollusk-svm --dev
根据需要包含特定程序的辅助工具:
cargo add mollusk-svm-programs-memo mollusk-svm-programs-token --dev
这些额外的 crate 提供了预配置的辅助工具,用于标准的 Solana 程序,减少了样板代码并简化了涉及代币操作或备忘指令的常见测试场景的设置。
⚠️ 在
cargo add <crate-name> --dev中的--dev标志用于通过将它们添加到[dev-dependencies]部分中来保持程序二进制文件的轻量化。 此配置确保测试工具不会增加程序的部署大小,同时在开发过程中提供对所有必要的 Solana 类型和辅助函数的访问。
一些 Solana crate 通过提供必要的类型和工具来增强测试体验:
cargo add solana-precompiles solana-account solana-pubkey solana-feature-set solana-program solana-sdk --dev
首先声明 program_id 并创建一个 Mollusk 实例,使用您在程序中使用的地址,以便正确调用并避免在测试期间抛出 "ProgramMismatch" 错误,以及构建程序的路径,如下所示:
use mollusk_svm::Mollusk;
use solana_sdk::pubkey::Pubkey;
const ID: Pubkey = solana_sdk::pubkey!("22222222222222222222222222222222222222222222");
// Alternative using an Array of bytes
// pub const ID: [u8; 32] = [
// 0x0f, 0x1e, 0x6b, 0x14, 0x21, 0xc0, 0x4a, 0x07,
// 0x04, 0x31, 0x26, 0x5c, 0x19, 0xc5, 0xbb, 0xee,
// 0x19, 0x92, 0xba, 0xe8, 0xaf, 0xd1, 0xcd, 0x07,
// 0x8e, 0xf8, 0xaf, 0x70, 0x47, 0xdc, 0x11, 0xf7,
// ];
#[test]
fn test() {
// Omit the `.so` file extension for the program name since
// it is automatically added when Mollusk is loading the file.
let mollusk = Mollusk::new(&ID, "target/deploy/program");
// Alternative using an Array of bytes
// let mollusk = Mollusk::new(&Pubkey::new_from_array(ID), "target/deploy/program")
}
在测试中,我们可以使用以下四种主要 API 方法之一:
process_instruction:处理指令并返回结果。process_and_validate_instruction:处理指令并对结果执行一系列检查,如果任何检查失败则触发 panic。process_instruction_chain:处理一系列指令并返回结果。process_and_validate_instruction_chain:处理一系列指令并对每个结果执行一系列检查,如果任何检查失败则触发 panic。但在使用这些方法之前,我们需要创建账户和指令结构以供传递:
在使用 Mollusk 测试 Solana 程序时,您将处理多种类型的账户,这些账户模拟了真实世界的程序执行场景。正确构建这些账户对于有效测试至关重要。
最基本的账户类型是 SystemAccount,它有两种主要变体:
系统账户不包含数据,并由 System Program 拥有。付款账户和未初始化账户的关键区别在于它们的 lamport 余额:付款账户有资金,而未初始化账户从空开始。
以下是在 Mollusk 中创建这些基本账户的方法:
use solana_sdk::{
account::Account,
system_program
};
// Payer account with lamports for transactions
let payer = Pubkey::new_unique();
let payer_account = Account::new(100_000_000, 0, &system_program::id());
// Uninitialized account with no lamports
let default_account = Account::default();
对于包含数据的 ProgramAccounts,您有两种构建方法:
use solana_sdk::account::Account;
let data = vec![
// Your serialized account data
];
let lamports = mollusk
.sysvars
.rent
.minimum_balance(data.len());
let program_account = Pubkey::new_unique();
let program_account_account = Account {
lamports,
data,
owner: ID, // The program's that owns the account
executable: false,
rent_epoch: 0,
};
创建账户后,将它们编译成 Mollusk 所需的格式:
let accounts = [
(user, user_account),
(program_account, program_account_account)
];
一旦了解了三个基本组件,为 Mollusk 测试创建指令就变得简单了:标识您的程序的 program_id,包含区分符和参数的 instruction_data,以及指定涉及账户及其权限的账户元数据。
以下是基本的指令结构:
use solana_sdk::instruction::{Instruction, AccountMeta};
let instruction = Instruction::new_with_bytes(
ID, // Your program's ID
&[0], // Instruction data (discriminator + parameters)
vec![AccountMeta::new(payer, true)], // Account metadata
);
指令数据必须包括指令区分符以及指令所需的任何参数。对于 Anchor 程序,默认区分符是从指令名称派生的 8 字节值。
为了简化 Anchor 判别器的生成,可以使用此辅助函数,并通过将判别器与序列化参数连接起来构造指令数据:
use sha2::{Sha256, Digest};
let instruction_data = &[
&get_anchor_discriminator_from_name("deposit"),
&1_000_000u64.to_le_bytes()[..],
]
.concat();
pub fn get_anchor_discriminator_from_name(name: &str) -> [u8; 8] {
let mut hasher = Sha256::new();
hasher.update(format!("global:{}", name));
let result = hasher.finalize();
[
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
]
}
对于 AccountMeta 结构体,我们需要根据账户权限使用适当的构造函数:
AccountMeta::new(pubkey, is_signer):用于可变账户AccountMeta::new_readonly(pubkey, is_signer):用于只读账户布尔参数指示账户是否必须签署交易。大多数账户是非签署账户(false),但需要授权操作的付款人和权限账户除外。
在准备好账户和指令后,现在可以使用 Mollusk 的执行 API 执行并验证您的程序逻辑。Mollusk 提供了四种不同的执行方法,具体取决于您是否需要验证检查以及是否测试单个或多个指令。
最简单的执行方法处理单个指令且不进行验证:
mollusk.process_instruction(&instruction, &accounts);
这将返回您可以手动检查的执行结果,但不会执行自动验证。
为了进行全面测试,请使用允许您指定预期结果的验证方法:
mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::success(), // Verify the transaction succeeded
Check::compute_units(5_000), // Expect specific compute usage
Check::account(&payer).data(&expected_data).build(), // Validate account data
Check::account(&payer).owner(&ID).build(), // Validate account owner
Check::account(&payer).lamports(expected_lamports).build(), // Check lamport balance
],
);
⚠️ 我们可以通过将多个检查“捆绑”在一起,像这样对同一账户执行多个检查:
Check::account(&payer).data(&expected_data).owner(&ID).build()
验证系统支持多种检查类型,以验证执行结果的不同方面。对于边界情况测试,您可以验证指令是否按预期失败:
mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::MissingRequiredSignature), // Expect specific error
],
);
对于需要多个指令的复杂工作流测试,请使用指令链方法:
mollusk.process_instruction_chain(
&[
(&instruction, &accounts),
(&instruction_2, &accounts_2)
]
);
结合多个指令与全面验证:
mollusk.process_and_validate_instruction_chain(&[
(&instruction, &accounts, &[Check::success()]),
(&instruction_2, &accounts_2, &[
Check::success(),
Check::account(&target_account).lamports(final_balance).build(),
]),
]);
Mollusk 提供灵活的初始化选项,以适应不同的测试场景。您可以创建预加载了您的程序的实例,或者从最小环境开始,根据需要添加组件。
在测试特定程序时,可以使用预加载程序的方式初始化 Mollusk:
use mollusk_svm::Mollusk;
use solana_sdk::pubkey::Pubkey;
const ID: Pubkey = solana_sdk::pubkey!("22222222222222222222222222222222222222222222");
#[test]
fn test() {
let mollusk = Mollusk::new(&ID, "target/deploy/program");
}
这种方法会自动加载您已编译的程序,并使其可用于测试,从而简化了针对特定程序测试套件的设置过程。
对于更广泛的测试场景,或者当您需要动态添加程序时,可以从默认实例开始:
use mollusk_svm::Mollusk;
#[test]
fn test() {
// System Program, ...
let mollusk = Mollusk::default();
}
默认实例包括像 System Program 这样的基本内置程序,为大多数 Solana 操作提供了基础,而不会增加不必要的程序负担。
当您的测试需要 System Program 时,Mollusk 提供了一个便捷的助手来生成必要的账户引用:
let (system_program, system_program_account) = keyed_account_for_system_program();
要复制程序加载功能或加载默认情况下不存在的自定义程序,您可以使用这些助手:
use mollusk_svm::Mollusk;
use mollusk_svm::program::create_program_account_loader_v3;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Get the account that you need
let program = &ID; // ID of the program we're trying to load into mollusk
let program_account = create_program_account_loader_v3(&ID);
// Load the program into your mollusk instance
mollusk.add_program(
&ID,
"target/deploy/program",
&mollusk_svm::program::loader_keys::LOADER_V3
);
}
Mollusk 的 token program 助手显著简化了涉及 SPL 代币的测试场景。mollusk-svm-programs-token crate 提供了对 Token、Token2022 和 Associated Token 程序的预配置支持。
在包含 token helper crate 后,添加您的测试所需的特定 token 程序:
use mollusk_svm::Mollusk;
#[test]
fn test() {
let mut mollusk = Mollusk::default();
// Add the SPL Token Program
mollusk_svm_programs_token::token::add_program(&mut mollusk);
// Add the Token2022 Program
mollusk_svm_programs_token::token2022::add_program(&mut mollusk);
// Add the Associated Token Program
mollusk_svm_programs_token::associated_token::add_program(&mut mollusk);
}
并创建测试场景所需的账户引用:
// SPL Token Program
let (token_program, token_program_account) =
mollusk_svm_programs_token::token::keyed_account();
// Token2022 Program
let (token2022_program, token2022_program_account) =
mollusk_svm_programs_token::token2022::keyed_account();
// Associated Token Program
let (associated_token_program, associated_token_program_account) =
mollusk_svm_programs_token::associated_token::keyed_account();
这些助手确保代币相关测试能够访问正确的程序账户并进行适当配置,从而无需手动设置程序即可全面测试代币操作。
在测试过程中,我们可能需要一个已经初始化的 Mint、Token 或 Associated Token 账户。幸运的是,Mollusk 为我们提供了一些方便的助手。
要创建一个Mint账户,我们可以像这样使用create_account_for_mint()函数:
use spl_token::state::Mint;
use mollusk_svm_programs_token::token::create_account_for_mint;
let mint_data = Mint {
mint_authority: Pubkey::new_unique(),
supply: 10_000_000_000,
decimals: 6,
is_initialized: true,
freeze_authority: None,
};
let mint_account = create_account_for_mint(mint_data)
要创建一个Token账户,我们可以像这样使用create_account_for_token_account()函数:
use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::token::create_account_for_token_account;
let token_data = TokenAccount {
mint: Pubkey::new_unique(),
owner: Pubkey::new_unique(),
amount: 1_000_000,
delegate: None,
state: AccountState::Initialized,
is_native: None,
delegated_amount: 0,
close_authority: None,
};
let token_account = create_account_for_token_account(token_data)
⚠️ 注意:这些示例适用于SPL-Token程序。如果您想创建由Token2022程序拥有的
Mint和Token账户,只需使用mollusk_svm_programs_token::token2022::...。
要创建一个Associated Token账户,我们可以像这样使用create_account_for_associated_token_account()函数:
use spl_token::state::{TokenAccount, AccountState};
use mollusk_svm_programs_token::associated_token::create_account_for_associated_token_account;
let token_data = TokenAccount {
mint: Pubkey::new_unique(),
owner: Pubkey::new_unique(),
amount: 1_000_000,
delegate: None,
state: AccountState::Initialized,
is_native: None,
delegated_amount: 0,
close_authority: None,
};
let associated_token_account = create_account_for_associated_token_account(token_data)
⚠️ 注意:此示例适用于SPL-Token程序。如果您想创建由Token2022程序拥有的
Associated Token账户,只需使用create_account_for_associated_token_2022_account函数。
Mollusk 包含一个专用的计算单元基准测试系统,可以精确测量和跟踪程序的计算效率。MolluskComputeUnitBencher 提供了一个简化的 API,用于创建全面的基准测试,监控不同指令场景下的计算单元消耗。
此基准测试系统对于性能优化尤为重要,因为它生成详细的报告,显示当前计算单元使用情况以及与之前运行的差异。
这使您能够立即看到代码更改对程序效率的影响,从而帮助您优化关键性能瓶颈。
基准测试工具可以无缝集成到您现有的 Mollusk 测试设置中:
use {
mollusk_svm_bencher::MolluskComputeUnitBencher,
mollusk_svm::Mollusk,
/* ... */
};
// Optionally disable logging.
solana_logger::setup_with("");
/* Instruction & accounts setup ... */
let mollusk = Mollusk::new(&program_id, "my_program");
MolluskComputeUnitBencher::new(mollusk)
.bench(("bench0", &instruction0, &accounts0))
.bench(("bench1", &instruction1, &accounts1))
.bench(("bench2", &instruction2, &accounts2))
.bench(("bench3", &instruction3, &accounts3))
.must_pass(true)
.out_dir("../target/benches")
.execute();
基准测试工具提供了多个配置选项:
must_pass(true):如果任何基准测试未能成功执行,将触发 panic,确保代码更改后基准测试仍然有效out_dir("../target/benches"):指定生成 markdown 报告的位置,便于与 CI/CD 系统和文档工作流集成要使用 cargo bench 运行基准测试,请在您的 Cargo.toml 中添加基准测试配置:
[[bench]]
name = "compute_units"
harness = false
基准测试工具生成的 markdown 报告提供了当前性能指标和历史比较:
| Name | CUs | Delta |
|--------|-------|--------|
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
报告格式包括:
Mollusk 支持创建和测试自定义系统调用,使您能够通过专用功能扩展 Solana 虚拟机以适应测试场景。
此功能对于测试通过 SIMD 添加的新系统调用(Syscall)的创建特别有价值,因为它可以模拟特定的运行时行为或创建受控的测试环境。
⚠️ 自定义系统调用在虚拟机(VM)级别操作,直接访问调用上下文和执行环境。
自定义系统调用是使用 declare_builtin_function! 宏定义的,该宏创建一个可以在 Mollusk 的运行时环境中注册的系统调用:
use {
mollusk_svm::{result::Check, Mollusk},
solana_instruction::Instruction,
solana_program_runtime::{
invoke_context::InvokeContext,
solana_sbpf::{declare_builtin_function, memory_region::MemoryMapping},
},
solana_pubkey::Pubkey,
};
declare_builtin_function!(
/// A custom syscall to burn compute units for testing
SyscallBurnCus,
fn rust(
invoke_context: &mut InvokeContext,
to_burn: u64,
_arg2: u64,
_arg3: u64,
_arg4: u64,
_arg5: u64,
_memory_mapping: &mut MemoryMapping,
) -> Result<u64, Box<dyn std::error::Error>> {
// Consume the specified number of compute units
invoke_context.consume_checked(to_burn)?;
Ok(0)
}
);
⚠️ 这是一个简单“消耗”计算单元(CUs)的自定义系统调用示例。
系统调用函数签名遵循特定模式:
invoke_context:提供对执行上下文和运行时状态的访问Result<u64, Box<dyn std::error::Error>>,指示成功或失败⚠️ 这就是所有系统调用在底层的创建方式
定义后,自定义系统调用必须在 Mollusk 的程序运行时环境中注册后才能使用:
#[test]
fn test_custom_syscall() {
std::env::set_var("SBF_OUT_DIR", "../target/deploy");
let program_id = Pubkey::new_unique();
let mollusk = {
let mut mollusk = Mollusk::default();
// Register the custom syscall with a specific name
mollusk
.program_cache
.program_runtime_environment
.register_function("sol_burn_cus", SyscallBurnCus::vm)
.unwrap();
// Add your program that uses the custom syscall
mollusk.add_program(
&program_id,
"test_program_custom_syscall",
&mollusk_svm::program::loader_keys::LOADER_V3,
);
mollusk
};
}
系统调用以一个名称(在此示例中为 "sol_burn_cus")注册,您的程序可以在进行系统调用时引用该名称。
自定义系统调用可以像其他程序功能一样进行测试,并具有对其行为进行精确控制的额外优势:
fn instruction_burn_cus(program_id: &Pubkey, to_burn: u64) -> Instruction {
Instruction::new_with_bytes(*program_id, &to_burn.to_le_bytes(), vec![])
}
#[test]
fn test_custom_syscall() {
// ... mollusk setup ...
// Establish baseline compute unit usage
let base_cus = mollusk
.process_and_validate_instruction(
&instruction_burn_cus(&program_id, 0),
&[],
&[Check::success()],
)
.compute_units_consumed;
// Test different compute unit consumption levels
for to_burn in [100, 1_000, 10_000] {
mollusk.process_and_validate_instruction(
&instruction_burn_cus(&program_id, to_burn),
&[],
&[
Check::success(),
Check::compute_units(base_cus + to_burn), // Verify exact CU consumption
],
);
}
}
⚠️ 此示例演示了测试一个消耗计算单元的系统调用,验证请求的单元数是否被精确消耗。验证精确数据的能力使 Mollusk 成为在实现之前测试自定义系统调用的最佳方式。
Mollusk 提供了全面的配置选项,使您能够自定义执行环境以满足特定的测试需求,正如我们从 Mollusk Context 中可以看到的:
/// Instruction context fixture.
pub struct Context {
/// The compute budget to use for the simulation.
pub compute_budget: ComputeBudget,
/// The feature set to use for the simulation.
pub feature_set: FeatureSet,
/// The runtime sysvars to use for the simulation.
pub sysvars: Sysvars,
/// The program ID of the program being invoked.
pub program_id: Pubkey,
/// Accounts to pass to the instruction.
pub instruction_accounts: Vec<AccountMeta>,
/// The instruction data.
pub instruction_data: Vec<u8>,
/// Input accounts with state.
pub accounts: Vec<(Pubkey, Account)>,
}
这些配置方法使您能够精确控制计算预算、功能可用性和系统变量,从而可以在各种运行时条件下测试程序。
use mollusk_svm::Mollusk;
use solana_sdk::feature_set::FeatureSet;
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Configure compute budget for performance testing
mollusk.set_compute_budget(200_000);
// Configure feature set to enable/disable specific Solana features
mollusk.set_feature_set(FeatureSet::all_enabled());
// Sysvars are handled automatically but can be customized if needed
}
计算预算决定了程序执行时可用的计算单元数量。这对于测试接近或超出计算限制的程序至关重要:
// Test with standard compute budget
mollusk.set_compute_budget(200_000);
Solana 的功能集控制了程序执行期间哪些区块链功能是激活的。Mollusk 允许您配置这些功能,以测试在不同网络状态下的兼容性:
use solana_sdk::feature_set::FeatureSet;
// Enable all features (latest functionality)
mollusk.set_feature_set(FeatureSet::all_enabled());
// All features disabled
mollusk.set_feature_set(FeatureSet::default());
有关可用功能的完整列表,请查阅 agave-feature-set crate 文档,其中详细说明了所有可配置的区块链功能及其影响。
Mollusk 提供了对所有系统变量(sysvars)的访问权限,程序在执行期间可以查询这些变量。虽然这些变量会自动配置为合理的默认值,但您可以根据特定的测试场景进行自定义:
/// Mollusk sysvars wrapper for easy manipulation
pub struct Sysvars {
pub clock: Clock, // Current slot, epoch, and timestamp
pub epoch_rewards: EpochRewards, // Epoch reward distribution info
pub epoch_schedule: EpochSchedule, // Epoch timing and slot configuration
pub last_restart_slot: LastRestartSlot, // Last validator restart information
pub rent: Rent, // Rent calculation parameters
pub slot_hashes: SlotHashes, // Recent slot hash history
pub stake_history: StakeHistory, // Historical stake activation data
}
您可以自定义特定的系统变量以测试与时间相关的逻辑、租金计算或其他依赖系统的行为,或者使用一些辅助工具:
#[test]
fn test() {
let mut mollusk = Mollusk::new(&program_id, "path/to/program.so");
// Customize clock for time-based testing
mollusk.sysvars.clock.epoch = 10;
mollusk.sysvars.clock.unix_timestamp = 1234567890;
// Jump to Slot 1000
mollusk.warp_to_slot(1000);
}