Okay, first off - if you're just starting with Solana testing, don't bother firing up solana test validator right away. Grab Bankrun instead. It's this lightweight NodeJS thing that spins up a fake Solana environment super fast, no real cluster needed. I usually drop it into my Anchor projects and tests fly. Why? Traditional validators chew through time and resources, especially when you're hammering hundreds of tests. Bankrun? Done in seconds. Sound familiar if you've waited forever for a local cluster to boot?
The thing is, Solana's speed means your tests gotta match that vibe. Bankrun lets you simulate program interactions, wallets, oracles - all without mainnet fees or devnet spam. Install it with npm i bankrun, and you're mocking real tx flows before lunch.
Look, Solana testing without Anchor? Possible, but kinda masochistic. Anchor handles the boilerplate - PDAs, serialization, all that jazz - and bakes in Mocha/Chai for JS tests. In my experience, 90% of folks use it because anchor test just works. Sets up a local validator, deploys your program, runs everything.
But here's the catch: default setup's slow for big suites. That's where we tweak. First, init a project: anchor init my solana test. Boom, tests folder ready. Now, edit Anchor.toml - set [test.validator.cluster_type = "localnet"] for speed, or point to devnet if you wanna feel fancy.
Honesty time: Anchor's tests are integration by default. They hit your actual program binary. Unit tests? Those are Rust side, inside #[cfg(test)] modules. We'll hit both.
anchor build --skip lint - skips unnecessary checks.RUSTFLAGS="-C opt level=3" to your env. Compiles scream.anchor test --skip local validator if you're using Bankrun or LiteSVM.Potential issue? Skipped lints might hide Rust warnings. Fix: Run full build weekly.
Super short ones first. In your lib.rs, wrap logic in private fns and test 'em raw.
#[test]: No Anchor needed, just cargo test.Take this King of the Hill example - simple game where you bid SOL to dethrone the king. Your becomeking fn needs to validate newprize > game_state.prize. Test it like:
Runs in milliseconds. Scale it: Test invariants like "prize always >0" or "king pubkey updates right". Fuzz? Throw in proptest crate for random inputs - finds edge cases humans miss.
In my experience, these save hours. One time, a lamport miscalc would've lost 0.5 SOL on devnet. Caught it here.
Now, this is where Solana shines - or breaks. Unit's fine, but programs CPI into each other, hit SPL tokens, PDAs. Integration tests mock that chaos.
Anchor way: In tests/my program.ts. Classic AAA pattern - Arrange (setup accounts), Act (call RPC), Assert (check state).
typescript import * as anchor from "@coral xyz/anchor"; it("Initializes the game", async () => { // Arrange const gameState = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("game_state")], program.programId ); // Act const tx = await program.methods .initialize(new anchor.BN(1000000)) // 0.001 SOL .accounts({ gameState, initialKing: provider.wallet.publicKey, prizePool: prizePoolPDA, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); // Assert const state = await program.account.gameState.fetch(gameState); console.log("King:", state.king.toBase58()); expect(state.prize.toNumber()).to.equal(1000000); });Run with anchor test. Tx sig spits out - copy it to Solana Explorer if curious. Fees? Localnet's free, but sim real ones: ~0.000005 SOL per tx signature.
Problem? Tests flaky on slow machines. Fix: Add await provider.connection.confirmTransaction(tx, "confirmed");. Or use Bankrun for consistency - it warps time, jumps slots. context.warptoslot(slotNumber). Wild.
| Tool | Speed | Use When | Gotchas |
|---|---|---|---|
| solana test validator | Slow (10-30s startup) | Full cluster sim | Eats RAM, port conflicts |
| Bankrun | Blazing (under 1s) | Multi program CPIs | NodeJS only |
| solana program test | Fast | Rust integration | Steep learning curve |
Alright, let's get hands on. Why Bankrun over vanilla? It bundles programs, mocks SPL, lets you pre fund accounts. Perfect for token transfers or oracle feeds.
Setup: npm i -D @solana/kit @solana developers/helpers bankrun. In your test:
const context = await BankrunProvider.init(..);await context.loadProgram(programId, programBinary);await context.warptoslot(100);VersionedTransaction.await context.processTransaction(tx);context.banksClient.getAccount.Example for our King game: Initialize, then bid higher. Watch the old king's lamports refund. If bid too low? ErrorCode::BidTooLow bubbles up - catch with expect().toThrow().
Issue I hit once: PDA bumps wrong in test env. Fix: Use Anchor's findProgramAddressSync everywhere consistently. What's next? Add mock SPL token program for prize in USDC.
Try this: Before coding become_king, write the failing test. Watch it red, green, refactor. Keeps tests behavior focused, not impl details. If you swap how prize transfers? Test still passes. Genius.
Okay, shifting gears. LiteSVM's this Rust beast - 25x faster than TS tests. Great for auditing or heavy sims. No JS overhead.
In a test crate: Cargo add lite svm, anchor litesvm. Setup fn:
rust #[tokio::test] async fn testescrow() { let env = setupescrowtest().await; let tx = env.buildinittx(); // Your helper env.processtx(tx).await.unwrap(); asserteq!(env.userbalance(), expected); }Pros: Parallel tests, direct SVM access. Cons: Rust only, more code. Use for property tests - "for all inputs X, output Y holds". Fees sim? Set compute units to 200k, mimic real ~1.4M CU limits.
Unit and integration catch most, but E2E? Full user flow. Deploy to localnet, hit with frontend sim or Cypress.
Steps: anchor deploy, then script tx via @solana/web3.js. Monitor with solana logs. Load test: Spam 100 bids/sec - check determinism (same input = same output).
Why does this matter? Solana's parallel execution means tx order flips can break stuff. E2E spots it. Tool tip: Helius RPC for devnet sims, free tier handles it.
Tests fail? Don't rage. First, solana logs - program logs spill errors. Tx sim: solana simulate --dry run, costs zilch.
Anchor debug: anchor test --log level debug. Rust panics? RUST_BACKTRACE=1 cargo test.
solana program show.In my experience, 70% bugs are signer checks or lamport math. Always assert balances pre/post: expect(after.lamports).to.eq(before.lamports + 1000000).
Basic tests miss randomness. Fuzz with Hypothesis (Python) or proptest (Rust). Define props: "Game always has a king", "Prize pool never negative".
Steps: Gen random bids 1-10 SOL, run 1000x. Failures? Refine code. QuickCheck style finds overflows fast - SOL's u64 lamports cap at ~9e15, but bids can wrap.
| Scenario | Pick This | Why |
|---|---|---|
| Simple units | Rust #[test] | Fastest |
| CPIs/tokens | Bankrun | Realistic mocks |
| Speed demons | LiteSVM | 25x boost |
| Full app | Anchor + Cypress | End to end |
Last bits. Mock oracles? Bankrun has setSysvar. Multi program? Load all binaries. CI/CD? GitHub Actions with Anchor cache - cuts build to 10s.
Common pit: Forgetting rent exempt min balance. Calc with rent.minimumBalance(space). Test it.
Scale up: Parallelize with jest --runInBand=false or Rust tokio. 500 tests? 2 mins flat.
Honestly, master this and your programs ship bug free. Hit snags? Tweak, rerun. You've got this.