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

407 lines
12 KiB
Markdown

# Solana Program Testing Overview
**High-level guide to testing Solana programs with the test pyramid structure**
This file provides an overview of Solana program testing, the testing pyramid structure, and the types of tests you should write. For specific implementation details and framework-specific guidance, see the related files.
---
## Related Testing Documentation
- **[Testing Frameworks](./testing-frameworks.md)** - Mollusk, LiteSVM, and Anchor testing implementations
- **[Testing Best Practices](./testing-practices.md)** - Best practices, common patterns, and additional resources
---
## Table of Contents
1. [Why Testing Matters](#why-testing-matters)
2. [Types of Tests](#types-of-tests)
3. [Testing Frameworks Available](#testing-frameworks-available)
4. [Test Structure Pyramid](#test-structure-pyramid)
---
## Why Testing Matters for Solana Programs
Solana programs are immutable after deployment and handle real financial assets. Comprehensive testing is critical to:
- **Prevent loss of funds**: Bugs in deployed programs can lead to irreversible financial losses
- **Ensure correctness**: Verify program logic works as intended under all conditions
- **Optimize performance**: Monitor compute unit usage to stay within Solana's limits (1.4M CU cap)
- **Build confidence**: Thorough testing enables safer deployments and upgrades
- **Catch edge cases**: Test boundary conditions, error handling, and attack vectors
---
## Types of Tests
**Unit Tests**
- Test individual functions and instruction handlers in isolation
- Fast, focused validation of specific logic
- Run frequently during development
**Integration Tests**
- Test complete instruction flows with realistic account setups
- Validate cross-program invocations (CPIs)
- Ensure proper state transitions
**Fuzz Tests**
- Generate random inputs to find edge cases and vulnerabilities
- Discover unexpected failure modes
- Test input validation thoroughly
**Compute Unit Benchmarks**
- Monitor compute unit consumption for each instruction
- Track performance regressions
- Ensure programs stay within CU limits
---
## Testing Frameworks Available
**Mollusk** (Recommended for both Anchor and Native Rust)
- Lightweight SVM test harness
- Exceptionally fast (no validator overhead)
- Works with both Anchor and native Rust programs
- Direct program execution via BPF loader
- Requires explicit account setup (no AccountsDB)
**LiteSVM** (Alternative for integration tests)
- In-process Solana VM for testing
- Available in Rust, TypeScript, and Python
- Faster than solana-program-test
- Supports RPC-like interactions
- Good for complex integration scenarios
**Anchor Test** (Anchor framework)
- TypeScript-based testing using @coral-xyz/anchor
- Integrates with local validator or LiteSVM
- Natural for testing Anchor programs from client perspective
- Slower but more realistic end-to-end tests
**solana-program-test** (Legacy)
- Full validator simulation
- More realistic but much slower
- Generally replaced by Mollusk and LiteSVM
**Recommendation**: Use Mollusk for fast unit and integration tests. Use LiteSVM or Anchor tests for end-to-end validation when needed.
---
## Test Structure Pyramid
### Overview
A production-grade Solana program should have a multi-level testing strategy. Each level serves a specific purpose and catches different types of bugs.
```
┌─────────────────────┐
│ Devnet/Mainnet │ ← Smoke tests
│ Smoke Tests │ (Manual, slow)
└─────────────────────┘
┌───────────────────────────┐
│ SDK Integration Tests │ ← Full transaction flow
│ (LiteSVM/TypeScript) │ (Seconds per test)
└───────────────────────────┘
┌─────────────────────────────────────┐
│ Mollusk Program Tests │ ← Instruction-level
│ (Unit + Integration in Rust) │ (~100ms per test)
└─────────────────────────────────────┘
┌───────────────────────────────────────────────┐
│ Inline Unit Tests (#[cfg(test)]) │ ← Pure functions
│ (Math, validation, transformations) │ (Milliseconds)
└───────────────────────────────────────────────┘
```
### Level 1: Inline Unit Tests
**Purpose:** Test pure functions in isolation - math, validation logic, data transformations.
**Location:** Inside your program code with `#[cfg(test)]`
**Why needed:**
- Instant feedback (milliseconds)
- Runs with `cargo test` - no build artifacts needed
- Catches arithmetic edge cases before they reach the SVM
- Documents expected behavior inline with code
**What belongs here:**
- Share calculations: `1_000_000 * 5000 / 10000 = 500_000`
- Overflow detection: `u64::MAX * 10000 = None`
- Rounding behavior: `100 * 1 / 10000 = 0` (floors)
- BPS (basis points) sum validation
- Data serialization/deserialization helpers
**What doesn't belong:**
- Account validation (needs ownership checks)
- CPI logic
- Full instruction execution
- State transitions
**Example:**
```rust
// In your program code (e.g., src/math.rs)
pub fn calculate_fee(amount: u64, fee_bps: u16) -> Option<u64> {
let fee = (amount as u128)
.checked_mul(fee_bps as u128)?
.checked_div(10_000)?;
Some(fee as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_fee_basic() {
assert_eq!(calculate_fee(1_000_000, 250), Some(25_000)); // 2.5%
assert_eq!(calculate_fee(1_000_000, 5000), Some(500_000)); // 50%
}
#[test]
fn test_calculate_fee_rounding() {
assert_eq!(calculate_fee(100, 1), Some(0)); // Rounds down
assert_eq!(calculate_fee(10_000, 1), Some(1)); // 0.01%
}
#[test]
fn test_calculate_fee_overflow() {
assert_eq!(calculate_fee(u64::MAX, 10000), None); // Would overflow
}
}
```
### Level 2: Mollusk Program Tests
**Purpose:** Test individual instructions with full account setup but without validator overhead.
**Location:** `tests/` directory or `#[cfg(test)]` modules
**Why needed:**
- Tests actual program binary execution
- Validates account constraints, signer checks, ownership
- ~100ms per test vs ~1s for full validator
- Catches instruction-level bugs
- Compute unit benchmarking
**What belongs here:**
- Each instruction handler (initialize, create_split, execute_split, etc.)
- Error conditions (wrong signer, invalid account owner)
- Account state transitions
- Cross-program invocations (CPIs)
- PDA derivation and signing
- Rent exemption validation
**Example:**
```rust
// tests/test_initialize.rs
use {
mollusk_svm::Mollusk,
my_program::{instruction::initialize, ID},
solana_sdk::{
account::Account,
instruction::Instruction,
pubkey::Pubkey,
},
};
#[test]
fn test_initialize_success() {
let mollusk = Mollusk::new(&ID, "target/deploy/my_program");
let user = Pubkey::new_unique();
let account = Pubkey::new_unique();
let instruction = initialize(&user, &account);
let accounts = vec![
(user, system_account(10_000_000)),
(account, Account::default()),
];
let result = mollusk.process_instruction(&instruction, &accounts);
assert!(result.is_ok());
}
#[test]
fn test_initialize_wrong_signer_fails() {
let mollusk = Mollusk::new(&ID, "target/deploy/my_program");
let user = Pubkey::new_unique();
let wrong_signer = Pubkey::new_unique();
let mut instruction = initialize(&user, &Pubkey::new_unique());
instruction.accounts[0].is_signer = false; // Missing signature
let accounts = vec![(user, system_account(10_000_000))];
let checks = vec![Check::instruction_err(
InstructionError::MissingRequiredSignature
)];
mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);
}
```
### Level 3: SDK Integration Tests
**Purpose:** Test that SDK produces correct instructions that work end-to-end.
**Location:** Separate SDK package (`sdk/tests/`) or TypeScript tests
**Why needed:**
- Validates serialization matches program expectations
- Tests full transaction flow (multiple instructions)
- Catches SDK bugs before users hit them
- Client-perspective testing
- Ensures TypeScript/Rust SDK matches program
**What belongs here:**
- SDK instruction builders produce valid transactions
- Full flows: create → deposit → execute
- Multiple instructions in one transaction
- Account resolution (finding PDAs from SDK)
- Error handling from client side
- Event parsing and decoding
**Example (LiteSVM):**
```rust
// sdk/tests/integration_test.rs
use {
litesvm::LiteSVM,
my_program_sdk::{instructions, MyProgramClient},
solana_sdk::{
signature::Keypair,
signer::Signer,
},
};
#[test]
fn test_full_flow_create_and_execute() {
let mut svm = LiteSVM::new();
// Add program
let program_bytes = include_bytes!("../../target/deploy/my_program.so");
svm.add_program(MY_PROGRAM_ID, program_bytes);
// Create client
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap();
let client = MyProgramClient::new(&svm, &payer);
// Step 1: Initialize
let tx1 = client.initialize().unwrap();
svm.send_transaction(tx1).unwrap();
// Step 2: Deposit
let tx2 = client.deposit(1_000_000).unwrap();
svm.send_transaction(tx2).unwrap();
// Step 3: Execute
let tx3 = client.execute().unwrap();
let result = svm.send_transaction(tx3).unwrap();
// Verify final state
let account = client.get_account().unwrap();
assert_eq!(account.balance, 1_000_000);
}
```
### Level 4: Devnet/Mainnet Smoke Tests
**Purpose:** Final validation in real environment.
**Location:** Manual testing or automated CI scripts
**Why needed:**
- Real RPC, real fees, real constraints
- Validates deployment configuration
- Tests against actual on-chain state
- Catches environment-specific issues
- Verifies upgrades work correctly
**What belongs here:**
- Post-deployment smoke tests (critical paths only)
- Upgrade validation (new version works)
- Integration with other mainnet programs
- Performance under real network conditions
**Example (Manual script):**
```bash
#!/bin/bash
# scripts/smoke-test-devnet.sh
echo "Running devnet smoke tests..."
# Test 1: Initialize
solana-keygen new --no-bpf-loader-deprecated --force -o /tmp/test-user.json
solana airdrop 2 /tmp/test-user.json --url devnet
my-program-cli initialize \
--program-id $PROGRAM_ID \
--payer /tmp/test-user.json \
--url devnet
# Test 2: Execute main flow
my-program-cli execute \
--amount 1000000 \
--payer /tmp/test-user.json \
--url devnet
echo "✅ Smoke tests passed"
```
### How to Use This Pyramid
**During development:**
1. Write inline tests as you implement math/validation
2. Write Mollusk tests for each instruction
3. Run frequently: `cargo test`
**Before PR/merge:**
1. Ensure all inline + Mollusk tests pass
2. Add SDK integration tests if SDK changed
3. Run compute unit benchmarks
**Before deployment:**
1. All tests pass on devnet-compatible build
2. Deploy to devnet
3. Run manual smoke tests on devnet
4. If pass, proceed to mainnet
**After deployment:**
1. Run smoke tests on mainnet
2. Monitor for errors
3. Keep tests updated as program evolves
### Benefits of This Structure
**Fast feedback loop:**
- Level 1 tests run in milliseconds
- Catch bugs early without slow iteration
**Comprehensive coverage:**
- Pure logic (Level 1)
- Program execution (Level 2)
- Client integration (Level 3)
- Real environment (Level 4)
**Efficient CI/CD:**
- Level 1-2 in every PR (fast)
- Level 3 on merge to main
- Level 4 post-deployment
**Clear responsibilities:**
- Each level tests different concerns
- No redundant tests
- Easier to maintain
---
## Next Steps
- For implementation details on Mollusk, LiteSVM, and Anchor testing, see **[Testing Frameworks](./testing-frameworks.md)**
- For best practices, common patterns, and additional resources, see **[Testing Best Practices](./testing-practices.md)**