Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/testing-frameworks.md
2025-11-30 09:01:25 +08:00

1256 lines
30 KiB
Markdown

# 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-overview.md)** - Testing pyramid structure and types of tests
- **[Testing Best Practices](./testing-practices.md)** - Best practices, common patterns, and additional resources
---
## Table of Contents
1. [Mollusk Testing](#mollusk-testing)
2. [Anchor-Specific Testing](#anchor-specific-testing)
3. [Native Rust Testing](#native-rust-testing)
---
## 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):**
```toml
[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):**
```toml
[dev-dependencies]
mollusk-svm = "0.5.1"
solana-sdk = "2.2" # Or "2.1" depending on your program
```
**For newer Solana versions (3.0+):**
```toml
[dev-dependencies]
mollusk-svm = "0.9" # Latest version
solana-sdk = "3.0"
```
**How to check your Solana SDK version:**
```bash
# 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:
```toml
[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:
```toml
[dev-dependencies]
mollusk-svm-bencher = "0.5.1" # Match mollusk-svm version
```
### Basic Test Structure
```rust
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
```rust
let result = mollusk.process_instruction(&instruction, &accounts);
```
**2. `process_and_validate_instruction`** - Execute and validate with checks
```rust
mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&checks,
);
```
**3. `process_instruction_chain`** - Execute multiple instructions sequentially
```rust
let result = mollusk.process_instruction_chain(
&[instruction1, instruction2, instruction3],
&accounts,
);
```
**4. `process_and_validate_instruction_chain`** - Execute chain with per-instruction checks
```rust
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:
```rust
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:**
```rust
let result = mollusk.process_instruction(&instruction, &accounts);
assert!(result.is_ok());
```
**With result inspection:**
```rust
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:**
```rust
use mollusk_svm::result::Check;
let checks = vec![
Check::success(), // Instruction succeeded
Check::compute_units(5000), // Exact compute units
];
```
**Account state checks:**
```rust
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:**
```rust
use solana_sdk::instruction::InstructionError;
let checks = vec![
Check::instruction_err(InstructionError::InvalidInstructionData),
];
```
**Complete validation example:**
```rust
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:**
```rust
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:**
```rust
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:
```bash
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:
```rust
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:
```rust
#[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:
```rust
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:
```rust
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:**
```rust
// Mollusk::default() includes subset of builtin programs
let mollusk = Mollusk::default(); // Includes System, BPF Loader, etc.
```
**All builtins:**
```toml
[dev-dependencies]
mollusk-svm = { version = "0.9", features = ["all-builtins"] }
```
**Adding specific programs:**
```rust
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:
```bash
# 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:**
1. `anchor build` - Build program
2. `anchor test` - Deploy to local validator and run TypeScript tests
3. Test files run against deployed program
4. Validator shuts down after tests complete
### TypeScript Tests with @coral-xyz/anchor
**Basic test structure:**
```typescript
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
```typescript
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
```typescript
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:**
```typescript
const tx = await program.methods
.initialize(bump)
.accounts({ /* ... */ })
.signers([user])
.rpc({
skipPreflight: false,
commitment: "confirmed",
});
```
### Reading Account State
```typescript
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
```typescript
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:**
```bash
cargo add litesvm --dev
```
**Basic usage:**
```rust
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:**
```rust
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:**
```rust
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:**
```rust
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`:
```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
1. **Use `anchor.workspace`**: Automatically loads program IDL
2. **Airdrop SOL in `before()` hooks**: Set up test accounts before tests
3. **Use proper commitment levels**: `confirmed` or `finalized` for reliability
4. **Test error conditions**: Use `.simulate()` to test expected failures
5. **Clean up between tests**: Reset account state or use fresh keypairs
6. **Use `--skip-build` during iteration**: Speed up test runs
7. **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:**
```toml
[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:**
```rust
// 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:
```rust
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:
```rust
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:**
```rust
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:**
```rust
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:**
```rust
let checks = vec![
Check::success(),
Check::compute_units(5000), // Exactly 5000 CU
];
```
**Data slice validation:**
```rust
// 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](./testing-overview.md)**
- For best practices, common patterns, and additional resources, see **[Testing Best Practices](./testing-practices.md)**