# 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-overview.md)** - Testing pyramid structure and types of tests - **[Testing Frameworks](./testing-frameworks.md)** - Mollusk, LiteSVM, and Anchor testing implementations --- ## Table of Contents 1. [Testing Best Practices](#testing-best-practices) 2. [Common Testing Patterns](#common-testing-patterns) 3. [Additional Resources](#additional-resources) --- ## 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:** ```rust // 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 { // ... } ``` ```rust // 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:** ```rust #[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:** ```rust #[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:** ```rust // 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:** ```yaml # .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:** ```bash # Native Rust cargo build-sbf && cargo test # Anchor anchor build && anchor test ``` **Run specific tests:** ```bash # Native Rust cargo test test_initialize # Anchor anchor test -- --test test_initialize ``` **Show program output:** ```bash # Native Rust cargo test -- --nocapture # Anchor anchor test -- --nocapture ``` **Run tests in parallel (be careful with shared state):** ```bash cargo test -- --test-threads=4 ``` --- ## Common Testing Patterns ### Testing PDAs **Anchor approach:** ```typescript 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:** ```rust #[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:** ```typescript 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](./testing-frameworks.md#testing-cpis) section in Testing Frameworks for a complete token transfer example. ### Testing Associated Token Accounts **Create ATA:** ```typescript 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:** ```rust #[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:** ```rust #[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 ```rust #[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 1. **Use Mollusk for fast, focused tests** - It's the recommended approach for both Anchor and native Rust programs 2. **Test early and often** - Catching bugs before deployment saves time and money 3. **Test error conditions** - Don't just test happy paths 4. **Monitor compute units** - Use benchmarking to catch performance regressions 5. **Organize tests logically** - Group by instruction, use helper modules 6. **Build before testing** - Always run `cargo build-sbf` or `anchor build` before tests 7. **Use validation checks** - Leverage the `Check` API for comprehensive validation 8. **Test with realistic data** - Use proper rent-exempt balances and realistic account states ### Quick Reference Commands ```bash # 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](./testing-overview.md)** - For framework-specific implementation details, see **[Testing Frameworks](./testing-frameworks.md)**