Testing Solana Programs
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.
Summary
- 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. - Key Concepts: test pyramid, in-process SVM, LiteSVM, Mollusk, Bankrun,
anchor test, local validator, Surfpool fork, fuzz / Trident, negative tests, compute units. - 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."
- Related Topics: LiteSVM, Mollusk, Bankrun (TS), Anchor TS Tests, Fuzzing Programs, Testing Against a Validator.
Foundations
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.
The test pyramid (Solana-shaped)
| Layer | Tools (this site) | Speed | Fidelity | Primary job |
|---|---|---|---|---|
| Unit / instruction | LiteSVM 0.6.x, Mollusk, pure Rust math tests | Fastest | SVM without full cluster | Handlers, errors, signers, CU smoke |
| Client integration | Bankrun / anchor-bankrun, focused Anchor TS | Fast | In-process + IDL/client | Method builders, accounts, multi-ix scripts |
| Integration / E2E | anchor test, solana-test-validator, Surfpool | Slowest | Full RPC, deploy, multi-tx | Deploy path, RPC semantics, forks |
| Property / adversarial | Trident, cargo-fuzz style | Long runs | Depends on harness | Parsing, math bounds, account flags |
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.
What "pass" must mean
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.
Mechanics & Interactions
LiteSVM: default fast Rust layer
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_programwith deploy.so(or include_bytes afteranchor 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: instruction-level Rust harness
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: TypeScript without a validator
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 TypeScript tests: IDL and product 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.
Validator and fork tests: high fidelity
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: adversarial input at scale
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.
How layers fit in a repo
programs/*/src/** → host unit tests + LiteSVM/Mollusk
tests/*.ts → Bankrun suite (fast) + optional validator suite
fuzz/ or trident/ → deposit/withdraw/swap targets
CI → cargo test (SVM) → bankrun → thin anchor test → nightly fuzzBuild once (anchor build / cargo build-sbf) and reuse the same .so across LiteSVM, Mollusk, and Bankrun so you never assert against stale bytecode.
Advanced Considerations & Applications
When to use each tool
| Need | Prefer | Skip when |
|---|---|---|
| Fast Rust negative tests | LiteSVM 0.6.x | You only care about TS client builders |
| Single-ix CU and result checks | Mollusk | You need multi-ix transaction scripts in TS |
| Fast Anchor/TS without ports | Bankrun | You must validate real RPC deploy semantics |
| IDL + full product path | anchor test + validator | Every commit needs sub-second feedback only |
| ALT, multi-tx, fork DeFi | Validator / Surfpool | Unit-level constraint bugs (fix lower first) |
| Pre-audit edge discovery | Trident / fuzz | No invariants or no time budget |
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.
Security-shaped test design
Map threat classes to layers:
- Signer/owner/mint substitution → LiteSVM or Mollusk (many cases, fast)
- PDA seed confusion → LiteSVM + one client test that derives seeds
- Arithmetic overflow and rounding → host property tests + fuzz
- CPI privilege mistakes → multi-program LiteSVM/Bankrun, then validator if loaders differ
- Client encoding mistakes → Bankrun/Anchor TS against IDL
Do not wait for an audit to invent negative cases. Catalog expected custom errors and write one failing path each.
CI, determinism, and coverage gaps
- 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
.soon 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.
Common Misconceptions
- "
anchor testalone 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.
FAQs
What is the Solana program testing pyramid?
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.
What should I read next in this section?
Start with LiteSVM and Mollusk for Rust speed, then Bankrun (TS) and Anchor TS Tests. Add Fuzzing Programs and Testing Against a Validator before production launch.
Related
- LiteSVM - fast in-process Rust program tests without a validator
- Mollusk - lightweight instruction-level Rust harness and checks
- Bankrun (TS) - TypeScript in-process tests with Anchor providers
- Anchor TS Tests -
anchor test, IDL-driven Mocha-style integration - Fuzzing Programs - Trident and fuzz targets for edge cases
- Testing Against a Validator - E2E against solana-test-validator and forks
Stack versions: This page was written for 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.