All articles
solanalitesvmtestingci

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

rust
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:

typescript
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.

rust
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 there

Set 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:

rust
// 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:

rust
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 + authorities

For 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

yaml
# .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 runs

Because 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

text
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.

Testing Solana programs with LiteSVM: the workflow, Rust and TS | devrels.xyz