Testing Solana programs with LiteSVM: the workflow, Rust and TS
The practical LiteSVM testing workflow — Rust harness, the @solana/web3.js TS bindings, time travel, account setup, CI integration, and migrating off bankrun.
LiteSVM is the in-process Solana VM that runs your tests in microseconds instead of the seconds solana-test-validator takes to boot. This article is the practical workflow — how to actually structure a test suite around it in Rust and TypeScript, set up state, control time, and wire it into CI.
The Rust harness
use litesvm::LiteSVM;
use solana_sdk::{
pubkey::Pubkey, signature::{Keypair, Signer},
transaction::Transaction, system_instruction,
};
#[test]
fn test_deposit() {
let mut svm = LiteSVM::new();
// Load your compiled program
let program_id = Pubkey::new_unique();
svm.add_program_from_file(program_id, "target/deploy/my_program.so").unwrap();
// Fund a payer
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
// Build + send the instruction
let ix = /* your program's deposit instruction */;
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer],
svm.latest_blockhash(),
);
let result = svm.send_transaction(tx);
assert!(result.is_ok());
// Inspect the resulting account state directly — no RPC round trip
let vault = svm.get_account(&vault_pda).unwrap();
let parsed = MyVault::try_from_slice(&vault.data).unwrap();
assert_eq!(parsed.balance, expected);
}Each test gets a fresh LiteSVM. No shared state, no teardown, no validator process. A 200-test suite runs in well under a second.
The TypeScript bindings
For teams whose tests are in JS/TS (Anchor's default), litesvm ships Node bindings:
import { LiteSVM } from "litesvm"
import { Keypair, PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js"
const svm = new LiteSVM()
svm.addProgramFromFile(programId, "target/deploy/my_program.so")
const payer = Keypair.generate()
svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL))
const tx = new Transaction()
tx.add(/* your instruction */)
tx.recentBlockhash = svm.latestBlockhash()
tx.sign(payer)
const result = svm.sendTransaction(tx)
if (result.constructor.name === "TransactionMetadata") {
// success — inspect logs + CUs
console.log("CUs:", result.computeUnitsConsumed())
console.log("logs:", result.logs())
} else {
// FailedTransactionMetadata
throw new Error(result.err().toString())
}
// Read account state synchronously
const acct = svm.getAccount(vaultPda)
console.log("lamports:", acct?.lamports)Drops into Jest, Vitest, or Bun test runners. Nobefore/after hooks to manage a validator lifecycle — the SVM is just an object you new up per test.
Setting arbitrary account state
The superpower for testing edge cases: write any account state directly, skipping the instructions that would normally produce it.
use solana_sdk::account::Account;
// Plant a pre-existing vault with a specific balance, mid-test
let vault_data = MyVault { owner: user.pubkey(), balance: 500_000, ..Default::default() };
let mut bytes = vec![];
vault_data.serialize(&mut bytes).unwrap();
svm.set_account(vault_pda, Account {
lamports: svm.minimum_balance_for_rent_exemption(bytes.len()),
data: bytes,
owner: program_id,
executable: false,
rent_epoch: 0,
}).unwrap();
// Now test the withdraw path against a vault that "already has" 500k
// without running the deposits to get thereSet up a corrupt account to test your validation, a maxed-out balance to test overflow handling, an account owned by the wrong program to test your ownership checks — all directly, in one line.
Time travel
Test time-locked logic (vesting, cooldowns, governance deadlines) without waiting:
// Read the current clock
let clock = svm.get_sysvar::<Clock>();
// Warp forward 1 epoch (~2 days) to test a vesting unlock
let mut new_clock = clock.clone();
new_clock.unix_timestamp += 2 * 24 * 60 * 60;
new_clock.epoch += 1;
svm.set_sysvar::<Clock>(&new_clock);
// Now the time-locked instruction should succeed
let result = svm.send_transaction(unlock_tx);
assert!(result.is_ok());Cloning mainnet accounts
For tests that need a real mainnet account (a token mint, an oracle), fetch it once and load it into the SVM:
use solana_client::rpc_client::RpcClient;
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com");
let usdc_mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
let account = rpc.get_account(&usdc_mint).unwrap();
svm.set_account(usdc_mint, account.into()).unwrap();
// Your tests now use the real USDC mint, with its real decimals + authoritiesFor heavier mainnet-fork needs (many accounts, live state, time control via RPC), graduate to Surfpool. LiteSVM is the fast unit-test layer; Surfpool is the realistic integration layer.
CI integration
# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install Solana CLI
run: sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
- name: Build program
run: cargo build-sbf
- name: Run LiteSVM tests
run: cargo test # no validator to spin up — just runsBecause there's no validator process, CI doesn't need to provision ports, wait for boot, or tear anything down. The test job is just cargo build-sbf && cargo test — fast and flake-free.
Migrating off bankrun / solana-test-validator
bankrun (solana-bankrun) → litesvm
─────────────────────────────────────────────────────────────
start(programs, accounts) new LiteSVM() + addProgramFromFile
context.banksClient svm (direct, synchronous)
banksClient.processTransaction svm.sendTransaction
context.setAccount svm.setAccount
banksClient.getAccount svm.getAccount
clock manipulation via context svm.setSysvar(Clock)
solana-test-validator → litesvm
─────────────────────────────────────────────────────────────
~10-30s boot per run instant (in-process)
RPC client + airdrop svm.airdrop (direct)
real slots/consensus none (skip — you're testing program logic)LiteSVM is largely API-compatible with bankrun (same lineage), so migration is mostly mechanical renames. The win is speed and the direct synchronous API.
When NOT to use it
- Testing RPC behaviour — LiteSVM has no RPC layer, no slots, no consensus. Use solana-test-validator or Surfpool.
- Multi-node / networking scenarios — there's no network. Single in-process VM only.
- Testing against live mainnet flux — clone static accounts, yes; live state, no. That's Surfpool's job.
References
For unit and integration tests of program logic, LiteSVM is the 2026 default. Microsecond runs, direct state control, time travel, and a CI job that's just cargo test.