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.
Related Testing Documentation
- Testing Overview - Testing pyramid structure and types of tests
- Testing Best Practices - Best practices, common patterns, and additional resources
Table of Contents
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:
anchor build- Build programanchor test- Deploy to local validator and run TypeScript tests- Test files run against deployed program
- 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
- Use
anchor.workspace: Automatically loads program IDL - Airdrop SOL in
before()hooks: Set up test accounts before tests - Use proper commitment levels:
confirmedorfinalizedfor reliability - Test error conditions: Use
.simulate()to test expected failures - Clean up between tests: Reset account state or use fresh keypairs
- Use
--skip-buildduring iteration: Speed up test runs - 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
- For the testing strategy overview and pyramid structure, see Testing Overview
- For best practices, common patterns, and additional resources, see Testing Best Practices