Okay, picture this: you're building a simple Solana program that should just transfer some tokens, but instead it fails with some cryptic compute unit error. You're staring at your terminal, scratching your head, thinking "what the hell?". Happened to me last week. Rent wasn't exempted right, accounts were misaligned, and mainnet fees were draining my wallet at like 0.000005 SOL per failed tx. Brutal. So I fired up local tests. Saved my ass. That's what we're doing here - getting you comfy with debugging Solana programs using local setups. No network costs, instant feedback, breakpoints that actually work.
In my experience, most bugs are stupid stuff: wrong account keys, overflow in borsh deserialization, or hitting that pesky 200k compute unit limit way too early. Local tests catch 'em before you burn real SOL. Why bother? Because deploying blind is gambling. And Solana's fast - bugs compound quick.
Look, writing tests from zero sucks. Clone this bad boy:
git clone git@github.com:mvines/solana bpf program template.gitcd solana bpf program templatecode . (or whatever editor you like)Now you've got a working example. Open src/lib.rs. It's dead simple - just logs incoming data with msg!. That's your canvas.
Scroll to the #[cfg(test)] bit. Hit "Run Tests" in VSCode. Boom, it builds and executes. Super fast. But boring. Let's make it debuggable.
So here's where it gets groovy. Set a breakpoint on that msg! line in process_instruction. Like line 11 or whatever.
instruction_data. Change stuff on the fly if you're feeling wild.CLI version? cargo test or cargo test bpf. Breakpoints ignored there, but good for CI. Thing is, this is bare bones runtime via solana program test. No real validator, so blockhashes are fake, system program acts basic. Fine for logic bugs. But for real world? Next level.
Had a program once that deserialized fine locally but bombed on devnet. Turns out account lengths off by 8 bytes. Local debug showed it instantly.
Now, solana program test is cool, but it skips validator niceties like real blockhashes or SPL programs. Enter solana test validator. Simulates the full chain on your machine. No fees, persistent ledger.
First, basics. Install Solana CLI if you haven't:
cargo install solana cli Then in your template dir:
solana config set --url http://localhost:8899cargo build bpf - builds your .so and keypair.solana test validator --bpf program target/deploy/bpfprogramtemplate keypair.json target/deploy/bpfprogramtemplate.soValidator's running. Logs spew everywhere. Your program loaded. Now what?
Open tests/integration.rs. Tweak it for editor debugging:
// #![cfg(feature = "test bpf")].addprogram("target/deploy/bpfprogramtemplate", programid)solanalogger::setupwithdefault("solanaruntime::message=debug");testvalidatortransaction() test.It boots a validator in process. Submits a tx Rust style. You'll see msg! outputs in the console. Set breakpoints inside your program code - they hit. Step over account loads, watch compute units tick up. In my experience, this catches 80% of issues.
| Local Runtime vs Validator | Speed | Realism | Best For |
|---|---|---|---|
| solana program test | Lightning (ms) | Basic | Unit logic, deserialization |
| solana test validator | Fast (secs) | Full chain sim | Tx flow, SPL interop, CU limits |
Okay, breakpoints rock, but sometimes you need logs for overview. Slap msg!("Hey, account balance: {:?}", balance); everywhere. Shows in validator output or test console.
But watch it - each msg! burns compute units. Like 300-500 CU per call. Stack too many, your tx fails at 1.4M CU cap (post-2024 limits). I usually litter 'em during dev, strip before deploy.
Pro move: solana_logger::setup() at test top. Routes everything to terminal. Levels: error!(), info!(), debug!(). Crank to debug for noise, dial back for sanity.
Common pitfall? Logs vanish if tx fails early. Use msg! before risky borsh calls. Sound familiar? That silent failure vibe.
Program panics? Check compute budget. Devnet tx costs ~0.000005 SOL, but local's free. Still, simulate: add Clock::get()?.slot checks.
Account not found? Validator starts clean. Pre fund with solana airdrop 10 on local. Or script it in tests.
Borsh errors drive me nuts. "Failed to deserialize" usually means wrong discriminator or padding. Local debug: print instruction_data.len(), compare expected.
Error handling? Define custom enums:
#[derive(Error, Debug, Copy, Clone)]
pub enum MyError { #[error("Wrong amount")] BadAmount,
}
Return Err(MyError::BadAmount.into()). Client sees it clear.
inccomputeused mocks if needed.Why? Mainnet doesn't forgive. One bad input, 0.000005 SOL gone.
Not pure Rust? No sweat. Validator running, hit it from JS.
Terminal 1: validator as above.
Terminal 2:
solana config set --url http://localhost:8899solana airdrop 2new Connection("http://localhost:8899"), build tx, send.Tail logs: solana logs in another terminal. See program output live. Perfect for fullstack debugging. Had a frontend calc rent wrong once - local proved it in 30 secs.
Want mainnet realism? Dump a program from chain, drop in tests/fixtures. Load via programtest.addprogramwithpath.
Ledger lives in ~/.cache/solana test validator usually. Fire up validator pointing there, hit localhost in Solana Explorer (set to local RPC). Inspect slots, txs visually. Game changer for state bugs.
Memory leaks? Rare in BPF, but if validator OOMs, nuke the ledger dir. Fresh start.
Daily grind:
solana logs.msg!, cargo test bpf all green → devnet.Common gotcha: BPF vs native. cargo test is native (fast), -bpf is real (slower, accurate). Always final run BPF.
Fees? Local: zero. Real: base 0.000005 SOL + rent. Compute over? Custom error or silent fail.
Stuck? solana test validator --help. Options galore: reset ledger, clone accounts, mock slots.
That's it. Your programs won't eat rent anymore. Hit a wall? Tweak, test local, repeat. You'll be mainnet ready fast. Go build something.