13 KiB
Solana Program Testing Best Practices
Common patterns, best practices, and additional testing resources
This file provides best practices for organizing tests, testing common scenarios, and efficiently running your test suite. For framework-specific details and the testing pyramid structure, see the related files.
Related Testing Documentation
- Testing Overview - Testing pyramid structure and types of tests
- Testing Frameworks - Mollusk, LiteSVM, and Anchor testing implementations
Table of Contents
Testing Best Practices
Test Organization
Organize by instruction:
tests/
├── test_initialize.rs
├── test_update.rs
├── test_transfer.rs
├── test_close.rs
└── helpers/
├── mod.rs
├── accounts.rs
└── instructions.rs
Use helper modules:
// tests/helpers/accounts.rs
use solana_sdk::{account::Account, pubkey::Pubkey};
pub fn system_account(lamports: u64) -> Account {
Account {
lamports,
data: vec![],
owner: solana_sdk::system_program::id(),
executable: false,
rent_epoch: 0,
}
}
pub fn token_account(/* ... */) -> Account {
// ...
}
// tests/test_initialize.rs
mod helpers;
use helpers::accounts::*;
#[test]
fn test_initialize() {
let accounts = vec![
(user, system_account(10_000_000)),
// ...
];
}
Edge Cases to Test
Account validation:
- Missing accounts
- Wrong account owner
- Account not writable when required
- Account not signer when required
- Uninitialized accounts
- Already initialized accounts
Numeric boundaries:
- Zero values
- Maximum values (u64::MAX)
- Overflow conditions
- Underflow conditions
- Negative results (when using signed integers)
Authorization:
- Missing signer
- Wrong signer
- Multiple signers
- PDA signer validation
State transitions:
- Invalid state transitions
- Idempotent operations
- Concurrent operations
- State rollback on error
Resource limits:
- Rent exemption
- Maximum account size
- Compute unit limits
- Stack depth limits (CPI)
Error Condition Testing
Test expected failures:
#[test]
fn test_insufficient_funds_fails() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let user = Pubkey::new_unique();
let accounts = vec![
(user, system_account(100)), // Not enough lamports
];
let instruction = /* create transfer instruction for 1000 lamports */;
let checks = vec![
Check::instruction_err(InstructionError::InsufficientFunds),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Test invalid data:
#[test]
fn test_invalid_instruction_data() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let instruction = Instruction {
program_id,
accounts: /* ... */,
data: vec![255, 255, 255], // Invalid instruction data
};
let checks = vec![
Check::instruction_err(InstructionError::InvalidInstructionData),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Compute Unit Monitoring
Set up continuous monitoring:
// benches/compute_units.rs
use mollusk_svm_bencher::MolluskComputeUnitBencher;
fn main() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let bencher = MolluskComputeUnitBencher::new(mollusk);
// Benchmark each instruction
bencher.bench(("initialize", &init_ix, &init_accounts));
bencher.bench(("update", &update_ix, &update_accounts));
bencher.bench(("close", &close_ix, &close_accounts));
bencher
.must_pass(true)
.out_dir("./target/benches")
.execute();
}
Add to CI/CD:
# .github/workflows/test.yml
- name: Run compute unit benchmarks
run: cargo bench
- name: Check for CU regressions
run: |
if git diff --exit-code target/benches/; then
echo "No compute unit changes"
else
echo "Compute unit usage changed - review carefully"
git diff target/benches/
fi
Running Tests Efficiently
Build before testing:
# Native Rust
cargo build-sbf && cargo test
# Anchor
anchor build && anchor test
Run specific tests:
# Native Rust
cargo test test_initialize
# Anchor
anchor test -- --test test_initialize
Show program output:
# Native Rust
cargo test -- --nocapture
# Anchor
anchor test -- --nocapture
Run tests in parallel (be careful with shared state):
cargo test -- --test-threads=4
Common Testing Patterns
Testing PDAs
Anchor approach:
it("derives PDA correctly", async () => {
const [pda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("seed"), user.publicKey.toBuffer()],
program.programId
);
await program.methods
.initialize(bump)
.accounts({
pda: pda,
user: user.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([user])
.rpc();
const accountData = await program.account.myAccount.fetch(pda);
expect(accountData.bump).to.equal(bump);
});
Native Rust approach:
#[test]
fn test_pda_derivation() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let user = Pubkey::new_unique();
let seeds = &[b"seed", user.as_ref()];
let (pda, bump) = Pubkey::find_program_address(seeds, &program_id);
let instruction = Instruction {
program_id,
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(pda, false),
AccountMeta::new_readonly(system_program::id(), false),
],
data: vec![0, bump], // Initialize instruction with bump
};
let accounts = vec![
(user, system_account(10_000_000)),
(pda, Account::default()),
];
let checks = vec![
Check::success(),
Check::account(&pda)
.owner(&program_id)
.build(),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Testing Token Operations
Anchor with SPL Token:
import { TOKEN_PROGRAM_ID, createMint, createAccount, mintTo } from "@solana/spl-token";
it("transfers tokens", async () => {
// Create mint
const mint = await createMint(
provider.connection,
wallet.payer,
wallet.publicKey,
null,
6
);
// Create token accounts
const sourceAccount = await createAccount(
provider.connection,
wallet.payer,
mint,
user.publicKey
);
const destAccount = await createAccount(
provider.connection,
wallet.payer,
mint,
recipient.publicKey
);
// Mint tokens
await mintTo(
provider.connection,
wallet.payer,
mint,
sourceAccount,
wallet.publicKey,
1_000_000
);
// Transfer via program
await program.methods
.transferTokens(new anchor.BN(500_000))
.accounts({
source: sourceAccount,
destination: destAccount,
authority: user.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([user])
.rpc();
// Verify balances
const sourceData = await getAccount(provider.connection, sourceAccount);
const destData = await getAccount(provider.connection, destAccount);
expect(sourceData.amount).to.equal(500_000n);
expect(destData.amount).to.equal(500_000n);
});
Native Rust with Mollusk: See the Testing CPIs section in Testing Frameworks for a complete token transfer example.
Testing Associated Token Accounts
Create ATA:
import { getAssociatedTokenAddress } from "@solana/spl-token";
it("creates associated token account", async () => {
const ata = await getAssociatedTokenAddress(
mint,
user.publicKey
);
await program.methods
.createAta()
.accounts({
ata: ata,
mint: mint,
owner: user.publicKey,
payer: wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
const account = await getAccount(provider.connection, ata);
expect(account.owner.toString()).to.equal(user.publicKey.toString());
expect(account.mint.toString()).to.equal(mint.toString());
});
Testing Account Validation
Validate account owner:
#[test]
fn test_wrong_owner_fails() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let account = Pubkey::new_unique();
let wrong_owner = Pubkey::new_unique();
let accounts = vec![
(account, Account {
lamports: 1_000_000,
data: vec![0; 100],
owner: wrong_owner, // Wrong owner!
executable: false,
rent_epoch: 0,
}),
];
let instruction = /* create instruction */;
let checks = vec![
Check::instruction_err(InstructionError::InvalidAccountOwner),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Validate signer:
#[test]
fn test_missing_signer_fails() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let user = Pubkey::new_unique();
let instruction = Instruction {
program_id,
accounts: vec![
AccountMeta::new(user, false), // Should be signer!
],
data: vec![],
};
let accounts = vec![
(user, system_account(1_000_000)),
];
let checks = vec![
Check::instruction_err(InstructionError::MissingRequiredSignature),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Testing Rent Exemption
#[test]
fn test_account_is_rent_exempt() {
let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
let account = Pubkey::new_unique();
let data_len = 100;
let rent = mollusk.sysvars.rent;
let rent_exempt_lamports = rent.minimum_balance(data_len);
let accounts = vec![
(account, Account {
lamports: rent_exempt_lamports,
data: vec![0; data_len],
owner: program_id,
executable: false,
rent_epoch: 0,
}),
];
let instruction = /* create instruction */;
let checks = vec![
Check::success(),
Check::account(&account)
.rent_exempt()
.build(),
];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
Additional Resources
Documentation
- Mollusk GitHub: https://github.com/anza-xyz/mollusk
- Mollusk Examples: https://github.com/anza-xyz/mollusk/tree/main/harness/tests
- Mollusk API Docs: https://docs.rs/mollusk-svm/latest/mollusk_svm/
- Anchor Testing Guide: https://www.anchor-lang.com/docs/testing
- LiteSVM: https://github.com/amilz/litesvm
- Solana Testing Docs: https://solana.com/docs/programs/testing
Key Takeaways
- Use Mollusk for fast, focused tests - It's the recommended approach for both Anchor and native Rust programs
- Test early and often - Catching bugs before deployment saves time and money
- Test error conditions - Don't just test happy paths
- Monitor compute units - Use benchmarking to catch performance regressions
- Organize tests logically - Group by instruction, use helper modules
- Build before testing - Always run
cargo build-sbforanchor buildbefore tests - Use validation checks - Leverage the
CheckAPI for comprehensive validation - Test with realistic data - Use proper rent-exempt balances and realistic account states
Quick Reference Commands
# Native Rust
cargo build-sbf # Build program
cargo test # Run tests
cargo test -- --nocapture # Run tests with output
cargo test test_name # Run specific test
cargo bench # Run compute unit benchmarks
# Anchor
anchor build # Build program
anchor test # Build, deploy, and test
anchor test --skip-build # Test without rebuilding
anchor test -- --nocapture # Test with logs
anchor test -- --test test_name # Run specific test
Next Steps
- For the testing strategy overview and pyramid structure, see Testing Overview
- For framework-specific implementation details, see Testing Frameworks