Testing Solana programs is not "run anchor test and ship." On-chain code fails on account metas, signers, PDAs, CPIs, and CU budgets that ordinary unit tests never touch. You need a stack of harnesses that trade fidelity for speed, then spend expensive validator time where RPC and multi-transaction behavior matter.
This page is the section umbrella. Sibling pages cover LiteSVM, Mollusk, Bankrun, Anchor TypeScript tests, fuzzing, CPI/PDA scenarios, and validator E2E in depth. Pin the stack this site uses: Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, and LiteSVM 0.6.x.
Structure program tests as a pyramid. Put most coverage in fast in-process SVM runners (LiteSVM, Mollusk, Bankrun), use Anchor TypeScript tests for IDL and client contracts, reserve solana-test-validator / Surfpool for E2E and mainnet-shaped forks, and fuzz hot paths before audit.
Insight: Validator-only suites are slow and flaky; pure host #[test] without an SVM misses Sealevel rules. Wrong layer choice either burns CI minutes or leaves signer, owner, and arithmetic bugs for mainnet-beta.
When to Use: Building or reviewing any Anchor 0.32.1 or native program; designing CI; pre-audit hardening; deciding whether a bug needs a Rust unit test or a full RPC path.
Limitations/Trade-offs: In-process VMs approximate but do not fully replicate gossip, ALTs, some sysvars, and cluster timing. Validator tests are high fidelity and slower. Fuzzing needs invariants and runtime budget, not only "random bytes."
Solana programs are sBPF executables the SVM runs under account locks and compute metering. A useful suite exercises instruction logic, account validation, client encoding, and multi-step workflows, each at the cheapest layer that still catches the bug class.
Read the pyramid bottom-up. Fix parse and signer bugs in LiteSVM or Mollusk in seconds. Prove the TypeScript client still builds the right metas with Bankrun or Anchor. Only then pay for validator spin-up. Run fuzz campaigns on deposit, withdraw, swap, and remaining-accounts paths before external review.
A green suite is not only happy paths. For every instruction that moves value or authority, include negative tests: missing signer, wrong owner, wrong mint, stale oracle, bad PDA bump, re-init, and unauthorized CPI. Assert typed error codes (Anchor #[error_code] or native ProgramError codes), not only "threw something." Where money moves, assert post-state balances and vault invariants, not only Ok(()).
Host-only tests (pure fee math, Borsh round-trips) still belong at the base. They do not replace loading a .so into an SVM and submitting a transaction or instruction.
LiteSVM 0.6.x embeds a Solana VM in the test process. You airdrop, load program bytes, set account state, sign transactions, and assert logs or errors without starting solana-test-validator.
Typical loop:
LiteSVM::new()
add_program with deploy .so (or include_bytes after anchor build / cargo build-sbf)
Airdrop payers; seed accounts with owners, data, and lamports
Build Instruction + Transaction, process, assert
Use LiteSVM for the bulk of Rust-facing coverage: discriminator routing, constraint failures, PDA seeds, and "attacker omitted is_signer" cases. Keep tests deterministic: fixed keypairs or seeded RNG, explicit clocks when your program reads Clock, and no dependence on wall-clock RPC latency.
Mollusk focuses on single-instruction execution against a compiled program with an explicit account store and optional result checks (success, failure, CU). Prefer it when you want a thinner surface than LiteSVM's transaction API, native-program teams living in #[test], or tight instruction CU regression checks.
LiteSVM is the broader in-process bank/tx tool; Mollusk is the lean instruction processor harness. Many codebases use both: Mollusk for pure ix tables, LiteSVM for multi-instruction transactions and richer bank setup.
Bankrun (and anchor-bankrun) gives TypeScript suites an in-process bank. startAnchor loads workspace programs; BankrunProvider plugs into Anchor's Program so .methods...rpc() does not need localhost:8899.
Reach for Bankrun when the team owns Mocha/Node + IDL types, when anchor test validator spin-up dominates CI, or when you want multi-instruction TypeScript flows without port races. It multiplies coverage of client-built transactions; keep a thinner validator suite for deploy and RPC-sensitive paths.
anchor test (Anchor 0.32.1) is the default product path: build, deploy to a local validator (unless skipped), run tests/*.ts against the workspace program and IDL. Use it for end-to-end user stories (init → deposit → withdraw), SPL Token / Token-2022 CPIs with real associated accounts, and IDL-driven client contracts.
Configure Anchor.toml consistently. Prefer --skip-local-validator in CI only when a managed solana-test-validator (or Surfpool) is already up with pinned Agave 4.1.1 binaries. Flakes often come from orphaned validators on 8899, leftover ledgers, or mixed CLI versions.
Clients outside Anchor may use @solana/kit 7.0.0 for RPC, simulation, and meta (computeUnitsConsumed, err). Kit is the client stack, not a program VM; pair it with Bankrun-style or validator harnesses as needed.
solana-test-validator (Solana CLI 3.0.10 / Agave 4.1.1) exercises real deploy, confirmation, sysvars, and multi-transaction sequencing. Use it for versioned transactions and ALTs, multi-tx product flows that in-process banks only approximate, RPC methods your app depends on, and pre-devnet/mainnet smoke.
Surfpool (site pin 0.12.0 where used) adds mainnet-fork local simulation so DeFi CPIs can see realistic pool state. Fork tests are heavier: pin pool addresses, document clone lists, and do not treat fork success as a security proof of your constraints.
Fuzzing (Trident for Anchor-oriented flows, cargo-fuzz style targets for parsers) mutates instruction data, amounts, and account flags while you assert invariants (vault never exceeds deposits, authority always required, no overflow path succeeds).
Place fuzzing across the pyramid: host parse/math when possible, instruction entry via Trident or SVM harnesses for account-flag combinations, and long pre-audit runs (max_total_time), not only a 30-second CI smoke. Fuzzing without invariants is noise; define fail-closed properties first.
Default for new teams: LiteSVM or Mollusk for Rust core, Bankrun for most TS, thin anchor test on a validator for E2E, fuzz hot money paths. Expand validator coverage when you adopt ALTs, multi-program CPIs against real Token state, or release checklists that require deploy smoke.
Pin Agave 4.1.1 / CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, LiteSVM 0.6.x in CI; laptop/pipeline drift is a top flake source.
Reset ledgers (solana-test-validator --reset) and kill port 8899 orphans.
Invalidate cached .so on program source and dependency changes.
Required PR job: fast SVM units (+ Bankrun if TS-heavy). Optional or scheduled: full validator E2E and long fuzz.
Line coverage is not enough. Track instructions without a negative path, errors never asserted by clients, CU ceilings on critical ix (Mollusk checks or Kit simulation meta), and product flows covered in only one language. When a public postmortem names a bug class (re-init, wrong account index, unchecked arithmetic), add a regression at the lowest layer that can express it.
"anchor test alone is a complete strategy." It is a strong integration layer, not a replacement for fast SVM unit tests or fuzzing.
"LiteSVM / Bankrun are toys; only the validator counts." In-process VMs catch most program logic bugs cheaply; use the validator for fidelity gaps, not everything.
"Green happy paths mean the program is safe." Adversarial account metas and amounts are the real test; omit them and you test demos, not security.
"Fuzzing replaces unit tests." Fuzzing explores state space; unit tests encode known requirements and regressions. You need both.
"Host #[test] without loading the program is enough." Pure math helps; Sealevel rules only show up when the SVM runs your entrypoint.
"Bankrun and LiteSVM are interchangeable clones." Same idea (in-process speed), different languages and APIs; choose by who owns the suite.
"Fork tests prove mainnet safety." They improve realism for CPIs against cloned state; they do not prove constraints against all account graphs.
"CU does not belong in tests." CU regressions on hot paths fail production like logic bugs; assert budgets where it matters.
"Matching versions is optional." Mixed Agave CLI, Anchor, and LiteSVM majors produce "works on my machine" failures.
A layered strategy: many fast in-process SVM tests (LiteSVM, Mollusk, Bankrun), fewer client/integration tests (anchor test / IDL), a thin high-fidelity validator or fork layer, plus fuzzing for adversarial edges.
When should I choose LiteSVM over Mollusk?
Choose LiteSVM for transaction-oriented setup, airdrops, and multi-instruction flows in Rust. Choose Mollusk for a minimal single-instruction harness and explicit result/CU checks. Many teams use both.
When should I choose Bankrun over `anchor test`?
Use Bankrun for fast TypeScript coverage without spinning solana-test-validator. Keep a smaller validator-backed anchor test suite for deploy, RPC, and multi-tx fidelity.
Does Anchor 0.32.1 require Mocha?
anchor test commonly runs Mocha-style TypeScript tests, but the important contract is provider + IDL + program deploy path. Bankrun can host the same Program API without a local validator process.
Can I use @solana/kit 7.0.0 in tests?
Yes for client construction, RPC against a validator, simulation, and reading transaction meta. Kit does not replace an in-process SVM by itself; pair it with a validator or Anchor/Bankrun providers as appropriate.
How do I test PDAs and CPIs effectively?
Derive PDAs the same way the program does, assert bumps and owners, and load multiple programs into LiteSVM or Bankrun for CPI. Escalate to a validator when Token or other native programs and real rent/sysvar behavior matter.
What belongs in fuzzing vs unit tests?
Unit tests lock known requirements and regressions with fixed cases. Fuzzing searches large input spaces (amounts, flags, remaining accounts) under explicit invariants. Start unit tests early; add fuzz before audit on money and parse paths.
Why are my validator tests flaky?
Common causes: leftover ledger state, port 8899 conflicts, unpinned CLI/Agave versions, wall-clock sleeps instead of confirmed commitments, and shared global keypairs. Reset the validator, pin Solana CLI 3.0.10 / Agave 4.1.1, and isolate fixtures.
Should every PR run the full validator suite?
Prefer required PR jobs for LiteSVM/Mollusk (and Bankrun if TS-heavy). Run full validator E2E on main, release branches, or scheduled pipelines if wall-clock cost is high. Never drop negative-path unit tests to "save CI."
How do I assert program errors correctly?
Match custom error codes from the program IDL or error enum. Avoid asserting only log string fragments. Negative tests should fail for the right reason.
Where does Surfpool fit relative to LiteSVM?
LiteSVM is for program-centric in-process tests. Surfpool approximates mainnet state via fork/RPC workflows. Use Surfpool when external account graphs matter; use LiteSVM for your program's rules.
Do I need fuzzing for a small program?
If the program holds value or parses complex remaining accounts, run at least a short pre-audit campaign. For a trivial counter with no financial risk, prioritize negative unit tests and property-test any math.
How do Rust and TypeScript suites stay consistent?
Share seed strings, account orders, and error catalogs. Build one .so per commit and load it in both ecosystems. When a bug is found in TS, add a LiteSVM regression so the failure is language-independent.