Skip to main content

How to Test Solana Programs with LiteSVM

Updated on
Nov 26, 2025

28 min read

Overview

Fast, reliable testing is essential as Solana programs grow more complex and teams automate more of their workflows. Traditional approaches often rely on solana-test-validator, Docker, or background processes that slow things down and make tests harder to reproduce across machines and CI environments.

LiteSVM solves this by running a full Solana runtime entirely inside your Rust test process. You get fine-grained control over accounts, slots, and sysvars, faster feedback loops, and tests that behave consistently anywhere with no external validator or extra setup required.

This guide walks you through using LiteSVM to test Anchor programs end-to-end in a faster, more streamlined way.

What You Will Do

You will learn everything you need to set up LiteSVM to test an Anchor program, including how to:


  • Set up LiteSVM inside a Rust test crate
  • Write program tests in Rust
  • Reduce boilerplate code with anchor-litesvm

What You Will Need

This guide assumes you have a basic understanding of Solana Programming, Anchor, Rust, and unit testing.

You should also have:


DependencyVersion
Solana CLI3.0.6
Anchor0.31.1
LiteSVM0.8.1
Node24.8.0
Rust1.90.0
solana-kite0.1.0
anchor-litesvm0.2.0

What is LiteSVM?

LiteSVM is a lightweight Solana virtual machine that runs inside your tests, so you can test programs without starting a separate validator. Embedding the VM removes overhead and executes tests quickly. It ships with an intuitive API and is available for Rust, TypeScript/JavaScript, and Python (via the solders library).

Key capabilities of LiteSVM:


  • Run unit tests in-process (no external validator)
  • Directly construct and manipulate accounts (System, PDAs, SPL Token)
  • Control time/slots, blockhash checks, and compute budgets for scenario testing
  • Simulate vs. execute transactions and assert on logs, events, and balances
  • Built-in profiling tools to spot performance issues

Adding LiteSVM to your project is as easy as including the LiteSVM crates:

LiteSVM Core (essential testing framework)

cargo add --dev litesvm

SPL Token support (optional)

cargo add --dev litesvm-token

The Anchor Escrow Program

We’ll use an existing Anchor Escrow program to learn how to implement tests using LiteSVM.

The Anchor Escrow program enables secure, trustless token swaps between two parties by locking tokens in a program-controlled vault until both parties fulfill the agreed terms. The sample program includes a token swap (initialize, exchange, cancel) and baseline tests.

The program implements three core instructions:


  1. make_offer: Allows a maker to create an offer by specifying how much of token A they want to trade for token B
  2. take_offer: Enables a taker to accept the offer and execute the swap
  3. refund_offer: Lets the maker cancel their offer and reclaim their tokens

Clone the repository:

git clone git@github.com:quiknode-labs/you-will-build-a-solana-program.git anchor-escrow-2025
cd anchor-escrow-2025

Run the existing tests to confirm your environment is set up correctly:

cargo test

Expected output:

running 11 tests

test test_id ... ok
test tests::test_same_token_mints_fails ... ok
test tests::test_zero_token_a_offered_amount_fails ... ok
test tests::test_take_offer_insufficient_funds_fails ... ok
test tests::test_duplicate_offer_id_fails ... ok
test tests::test_make_offer_succeeds ... ok
test tests::test_insufficient_funds_fails ... ok
test tests::test_non_maker_cannot_refund_offer ... ok
test tests::test_zero_token_b_wanted_amount_fails ... ok
test tests::test_take_offer_success ... ok
test tests::test_refund_offer_success ... ok

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

Test Structure

Each test follows a consistent pattern:


  1. Initialize test environment: Call setup_escrow_test() to create a fresh EscrowTestEnvironment with LiteSVM, deploy program, mint tokens, and fund user accounts.
  2. Execute program instructions: Use helper functions to build and send transactions.
  3. Assert expected outcomes: Verify balances, account states, and transaction results.

Test Helper Functions

The helper functions for our tests are located at programs/escrow/src/escrow_test_helpers.rs.

Here are some key test helpers you’ll use:


  • EscrowTestEnvironment: A compact fixture that packages the test LiteSVM instance, program ID, two token mints, funded users (alice/bob), and their ATAs.
  • setup_escrow_test: A factory that creates and boots the environment, mints, funds users and their ATAs, and returns a ready-to-use EscrowTestEnvironment.
  • Discriminator Helpers: Since the project’s existing tests don’t use an Anchor client, we have to manually add Anchor’s 8-byte discriminators. These helpers compute and prefix them so each call routes to the correct handler.
  • Execute Helpers: High-level functions that derive PDAs, build instruction data, and send transactions for make, take, and refund.

Make Offer Tests

test_make_offer_succeeds validates the basic offer creation flow.

The test sets up the environment, generates a unique offer ID, derives the necessary PDAs (offer account and vault), builds the accounts structure, creates an instruction for Alice to offer 1 TOKEN_A for 1 TOKEN_B, sends the transaction, and asserts success.

programs/escrow/src/tests.rs
#[test]
fn test_make_offer_succeeds() {
let mut test_environment = setup_escrow_test();

let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
1 * TOKEN_A,
1 * TOKEN_B,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);

assert!(result.is_ok(), "Valid offer should succeed");
}

test_duplicate_offer_id_fails ensures offer IDs are unique.

Alice first creates an offer successfully with a specific ID, then Bob attempts to create a different offer using the same ID (which would derive to the same PDA), and the test verifies this second transaction fails because the account already exists.

programs/escrow/src/tests.rs
#[test]
fn test_duplicate_offer_id_fails() {
let mut test_environment = setup_escrow_test();

let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
1 * TOKEN_A,
1 * TOKEN_B,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(result.is_ok(), "First offer should succeed");

let make_offer_accounts_with_existing_offer_id = build_make_offer_accounts(
test_environment.bob.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.bob_token_account_a,
offer_account,
vault,
);

let make_offer_instruction_with_existing_offer_id = build_make_offer_instruction(
offer_id,
1 * TOKEN_A,
1 * TOKEN_B,
make_offer_accounts_with_existing_offer_id,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction_with_existing_offer_id],
&[&test_environment.bob],
&test_environment.bob.pubkey(),
);
assert!(result.is_err(), "Second offer with same ID should fail");
}

test_insufficient_funds_fails checks balance validation.

Alice attempts to create an offer for 1000 TOKEN_A despite only having 10 TOKEN_A in her account, and the test confirms the transaction fails due to insufficient funds.

programs/escrow/src/tests.rs
#[test]
fn test_insufficient_funds_fails() {
let mut test_environment = setup_escrow_test();

// Try to create offer with more tokens than Alice owns
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
1000 * TOKEN_A, // Try to offer 1000 tokens (Alice only has 10)
1 * TOKEN_B,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(result.is_err(), "Offer with insufficient funds should fail");
}

test_same_token_mints_fails enforces distinct token types.

The test builds an offer where both TOKEN_A and TOKEN_B point to the same mint, and verifies the program rejects this invalid configuration.

programs/escrow/src/tests.rs
#[test]
fn test_same_token_mints_fails() {
let mut test_environment = setup_escrow_test();

// Try to create offer with same token mint for both token_a and token_b
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_a.pubkey(), // Same mint for both
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction =
build_make_offer_instruction(offer_id, 1 * TOKEN_A, 1 * TOKEN_B, make_offer_accounts);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(result.is_err(), "Offer with same token mints should fail");
}

test_zero_token_b_wanted_amount_fails validates non-zero wanted amounts.

The test creates an offer requesting 0 TOKEN_B in exchange for 1 TOKEN_A and confirms the program rejects zero-amount offers.

programs/escrow/src/tests.rs
#[test]
fn test_zero_token_b_wanted_amount_fails() {
let mut test_environment = setup_escrow_test();

// Try to create offer with zero token_b_wanted_amount
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
1 * TOKEN_A,
0, // Zero wanted amount
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(
result.is_err(),
"Offer with zero token_b_wanted_amount should fail"
);
}

test_zero_token_a_offered_amount_fails validates non-zero offered amounts.

Similar to the previous test, this creates an offer with 0 TOKEN_A offered for 1 TOKEN_B and verifies rejection.

programs/escrow/src/tests.rs
#[test]
fn test_zero_token_a_offered_amount_fails() {
let mut test_environment = setup_escrow_test();

// Try to create offer with zero token_a_offered_amount
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
0, // Zero offered amount
1 * TOKEN_B,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(
result.is_err(),
"Offer with zero token_a_offered_amount should fail"
);
}

Take Offer Tests

test_take_offer_success validates the complete swap flow.

Alice creates an offer for 3 TOKEN_A wanting 2 TOKEN_B, Bob accepts it, and the test verifies final balances (Alice: 7 TOKEN_A and 2 TOKEN_B, Bob: 3 TOKEN_A and 3 TOKEN_B) and confirms the offer account is closed.

programs/escrow/src/tests.rs
#[test]
fn test_take_offer_success() {
let mut test_environment = setup_escrow_test();

// Alice creates an offer: 3 token A for 2 token B
let offer_id = generate_offer_id();
let alice = test_environment.alice.insecure_clone();
let alice_token_account_a = test_environment.alice_token_account_a;
let (offer_account, vault) = execute_make_offer(
&mut test_environment,
offer_id,
&alice,
alice_token_account_a,
3 * TOKEN_A,
2 * TOKEN_B,
).unwrap();

// Bob takes the offer
let bob = test_environment.bob.insecure_clone();
let bob_token_account_a = test_environment.bob_token_account_a;
let bob_token_account_b = test_environment.bob_token_account_b;
let alice_token_account_b = test_environment.alice_token_account_b;
execute_take_offer(
&mut test_environment,
&bob,
&alice,
bob_token_account_a,
bob_token_account_b,
alice_token_account_b,
offer_account,
vault,
).unwrap();

// Check balances
assert_token_balance(
&test_environment.litesvm,
&test_environment.alice_token_account_a,
7 * TOKEN_A,
"Alice should have 7 token A left",
);
assert_token_balance(
&test_environment.litesvm,
&test_environment.alice_token_account_b,
2 * TOKEN_B,
"Alice should have received 2 token B",
);
assert_token_balance(
&test_environment.litesvm,
&test_environment.bob_token_account_a,
3 * TOKEN_A,
"Bob should have received 3 token A",
);
assert_token_balance(
&test_environment.litesvm,
&test_environment.bob_token_account_b,
3 * TOKEN_B,
"Bob should have 3 token B left",
);

// Check that the offer account is closed after being taken
check_account_is_closed(
&test_environment.litesvm,
&offer_account,
"Offer account should be closed after being taken"
);
}

test_take_offer_insufficient_funds_fails ensures takers have sufficient balance.

Alice creates an offer wanting 1000 TOKEN_B (Bob only has 5), Bob attempts to accept it, and the test confirms the transaction fails due to Bob's insufficient funds.

programs/escrow/src/tests.rs
#[test]
fn test_take_offer_insufficient_funds_fails() {
let mut test_environment = setup_escrow_test();

// Create an offer from Alice for a large amount of token B
let large_token_b_amount = 1000 * TOKEN_B; // Much larger than Bob's balance (he has 5)
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
1 * TOKEN_A,
large_token_b_amount,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(result.is_ok(), "Alice's offer should succeed");

// Try to take the offer with Bob who has insufficient token B
let take_offer_accounts = TakeOfferAccounts {
associated_token_program: spl_associated_token_account::ID,
token_program: spl_token::ID,
system_program: anchor_lang::system_program::ID,
taker: test_environment.bob.pubkey(),
maker: test_environment.alice.pubkey(),
token_mint_a: test_environment.token_mint_a.pubkey(),
token_mint_b: test_environment.token_mint_b.pubkey(),
taker_token_account_a: test_environment.bob_token_account_a,
taker_token_account_b: test_environment.bob_token_account_b,
maker_token_account_b: test_environment.alice_token_account_b,
offer_account,
vault,
};

let take_offer_instruction = build_take_offer_instruction(take_offer_accounts);
let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![take_offer_instruction],
&[&test_environment.bob],
&test_environment.bob.pubkey(),
);
assert!(
result.is_err(),
"Take offer with insufficient funds should fail"
);
}

Refund Offer Tests

test_refund_offer_success validates the refund mechanism.

Alice creates an offer locking 3 TOKEN_A (balance drops to 7), refunds it, and the test verifies her balance returns to 10 TOKEN_A and the offer account is closed.

programs/escrow/src/tests.rs
#[test]
fn test_refund_offer_success() {
let mut test_environment = setup_escrow_test();

// Alice creates an offer: 3 token A for 2 token B
let offer_id = generate_offer_id();
let alice = test_environment.alice.insecure_clone();
let alice_token_account_a = test_environment.alice_token_account_a;
let (offer_account, vault) = execute_make_offer(
&mut test_environment,
offer_id,
&alice,
alice_token_account_a,
3 * TOKEN_A,
2 * TOKEN_B,
).unwrap();

// Check that Alice's balance decreased after creating the offer
assert_token_balance(
&test_environment.litesvm,
&test_environment.alice_token_account_a,
7 * TOKEN_A,
"Alice should have 7 token A left after creating offer",
);

// Alice refunds the offer
execute_refund_offer(
&mut test_environment,
&alice,
alice_token_account_a,
offer_account,
vault,
).unwrap();

// Check that Alice's balance is restored after refunding
assert_token_balance(
&test_environment.litesvm,
&test_environment.alice_token_account_a,
10 * TOKEN_A,
"Alice should have all 10 token A back after refunding",
);

// Check that the offer account is closed
check_account_is_closed(
&test_environment.litesvm,
&offer_account,
"Offer account should be closed after refund"
);
}

test_non_maker_cannot_refund_offer enforces authorization.

Alice creates an offer, Bob attempts to refund it by signing with his keypair, the test confirms the transaction fails, and verifies Alice's balance remains at 7 TOKEN_A with the offer account still existing.

programs/escrow/src/tests.rs
#[test]
fn test_non_maker_cannot_refund_offer() {
let mut test_environment = setup_escrow_test();

// Alice creates an offer: 3 token A for 2 token B
let offer_id = generate_offer_id();
let (offer_account, _offer_bump) = get_pda_and_bump(&seeds!["offer", offer_id], &test_environment.program_id);
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&test_environment.token_mint_a.pubkey(),
);

let make_offer_accounts = build_make_offer_accounts(
test_environment.alice.pubkey(),
test_environment.token_mint_a.pubkey(),
test_environment.token_mint_b.pubkey(),
test_environment.alice_token_account_a,
offer_account,
vault,
);

let make_offer_instruction = build_make_offer_instruction(
offer_id,
3 * TOKEN_A,
2 * TOKEN_B,
make_offer_accounts,
);

let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![make_offer_instruction],
&[&test_environment.alice],
&test_environment.alice.pubkey(),
);
assert!(result.is_ok(), "Alice's offer should succeed");

// Bob tries to refund Alice's offer (should fail)
let refund_offer_accounts = RefundOfferAccounts {
token_program: spl_token::ID,
system_program: anchor_lang::system_program::ID,
maker: test_environment.bob.pubkey(),
token_mint_a: test_environment.token_mint_a.pubkey(),
maker_token_account_a: test_environment.alice_token_account_a,
offer_account,
vault,
};

let refund_instruction = build_refund_offer_instruction(refund_offer_accounts);
let result = send_transaction_from_instructions(
&mut test_environment.litesvm,
vec![refund_instruction],
&[&test_environment.bob],
&test_environment.bob.pubkey(),
);
assert!(
result.is_err(),
"Non-maker should not be able to refund an offer"
);

// Verify that Alice's balance is still the same (offer not refunded)
assert_token_balance(
&test_environment.litesvm,
&test_environment.alice_token_account_a,
7 * TOKEN_A,
"Alice's balance should remain unchanged after failed refund attempt",
);

// Verify that the offer account still exists (invert the check)
let offer_account_data = test_environment.litesvm.get_account(&offer_account);
assert!(
offer_account_data.is_some() && !offer_account_data.unwrap().data.is_empty(),
"Offer account should still exist after failed refund attempt"
);
}

Other LiteSVM Features

LiteSVM provides powerful features that go beyond the example tests in this guide. The next sections showcase some of the advanced techniques you can implement in your tests for greater control.

Creating System and Program Accounts

You often need a funded payer and uninitialized accounts for PDAs. With LiteSVM you can airdrop in place and write raw account bytes when needed.

use solana_account::Account;

let pda_key = Keypair::new().pubkey();
svm.set_account(
pda_key,
Account {
lamports: 100_000_000_000,
data: vec![0u8; 128],
owner: PROGRAM_ID,
executable: false,
rent_epoch: 0,
},
);

Creating SPL Token Mints and Accounts

If your flow uses SPL Token, serialize Mint and Account with Pack. Set owners correctly to TOKEN_PROGRAM_ID and make the accounts rent-exempt or simply overfund.

use spl_token::{ID as TOKEN_PROGRAM_ID, state::{Mint, Account as TokenAccount}};
use solana_program_pack::Pack;
use solana_keypair::Keypair;

let mint = Keypair::new();
let mut mint_bytes = vec![0; Mint::LEN];
Mint::pack(
Mint { mint_authority: None.into(), supply: 0, decimals: 6, is_initialized: true, freeze_authority: None.into() },
&mut mint_bytes
).unwrap();

svm.set_account(
mint.pubkey(),
solana_account::Account { lamports: svm.minimum_balance_for_rent_exemption(Mint::LEN), data: mint_bytes, owner: TOKEN_PROGRAM_ID, executable: false, rent_epoch: 0 }
);

let token_acc = Keypair::new();
let owner = Keypair::new();
let mut token_bytes = vec![0; TokenAccount::LEN];
TokenAccount::pack(
TokenAccount {
mint: mint.pubkey(),
owner: owner.pubkey(),
amount: 0,
delegate: None.into(),
state: spl_token::state::AccountState::Initialized,
is_native: None.into(),
delegated_amount: 0,
close_authority: None.into(),
},
&mut token_bytes
).unwrap();

svm.set_account(
token_acc.pubkey(),
solana_account::Account { lamports: svm.minimum_balance_for_rent_exemption(TokenAccount::LEN), data: token_bytes, owner: TOKEN_PROGRAM_ID, executable: false, rent_epoch: 0 }
);

Simulate Versus Execute and Assert

Use simulation to preview logs and instruction errors before committing. Execute to update the in-memory ledger and assert on account data and balances. Read back accounts and parse your structs for verification.

let res = svm.send_transaction(tx).unwrap();
assert!(res.logs.iter().any(|l| l.contains("Event:EscrowInitialized")));

let acc = svm.get_account(&pda_key).expect("exists");
assert_eq!(acc.owner, PROGRAM_ID);

Controlling Time, Slots, Blockhash, and Compute

Reproduce expiry paths, stale orders, and compute regressions by adjusting sysvars and runtime configuration. Move time forward by mutating Clock, jump to a future slot, expire the blockhash to test error handling, and tune compute budgets when profiling.

use solana_sdk::clock::Clock;

let mut clock: Clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp += 3600;
svm.set_sysvar::<Clock>(&clock);

svm.warp_to_slot(500);
svm.expire_blockhash();

let mut budget = litesvm::ComputeBudget::default();
budget.compute_unit_limit = 2_000_000;
svm.with_compute_budget(budget);

svm.with_sigverify(true);
svm.with_blockhash_check(true);

Using Anchor LiteSVM

Testing Anchor programs with raw LiteSVM requires you to manually create and fund accounts, build and sign transactions, calculate rent, handle SPL token operations, and repeat setup code. anchor-litesvm wraps LiteSVM in a high-level API with helpers that abstract those steps.

Key differences:


  • Program loading: Takes typed args and derives discriminator and serialization automatically.
  • Accounts list: Accepts a typed accounts::... struct in any order, infers flags, and orders accounts for you.
  • Transaction assembly: Collapses this to a fluent program().accounts().args().send() call.
  • Imports and helpers: Trims imports to the client plus typed accounts::... and instruction::... items.
  • Boilerplate and safety: Centralizes discriminator math, metas, and transaction wiring, and ties them to your Anchor types for safer changes.

Let's rewrite our test_make_offer_succeeds test using anchor-litesvm.

touch programs/escrow/src/anchor_tests.rs

Now add the following code to anchor_tests.rs:

use anchor_litesvm::{AnchorLiteSVM, TestHelpers};
use solana_signer::Signer;
use solana_pubkey::Pubkey;
use solana_instruction::{AccountMeta, Instruction};
use std::str::FromStr;
use std::fs;

#[cfg(test)]
mod tests {
use super::*;

const PROGRAM_ID: &str = "8jR5GeNzeweq35Uo84kGP3v1NcBaZWH5u62k7PxN4T2y";
const TOKEN_A: u64 = 1_000_000_000; // 1 token with 9 decimals
const TOKEN_B: u64 = 1_000_000_000; // 1 token with 9 decimals

#[test]
fn test_make_offer_succeeds() {
// Initialize AnchorLiteSVM with the escrow program
let program_id = Pubkey::from_str(PROGRAM_ID).unwrap();
let program_data = fs::read("../../target/deploy/escrow.so").unwrap();
let mut ctx = AnchorLiteSVM::build_with_program(program_id, &program_data);

// Create funded accounts using anchor-litesvm helpers
let mint_authority = ctx.svm.create_funded_account(1_000_000_000).unwrap();
let alice = ctx.svm.create_funded_account(1_000_000_000).unwrap();

// Create token mints using anchor-litesvm helpers
let token_mint_a = ctx.svm.create_token_mint(&mint_authority, 9).unwrap();
let token_mint_b = ctx.svm.create_token_mint(&mint_authority, 9).unwrap();

// Create associated token account for Alice (token A)
let alice_token_account_a = ctx.svm
.create_associated_token_account(&token_mint_a.pubkey(), &alice)
.unwrap();

// Mint tokens to Alice's token account
ctx.svm
.mint_to(
&token_mint_a.pubkey(),
&alice_token_account_a,
&mint_authority,
10 * TOKEN_A, // Alice gets 10 token A
)
.unwrap();

// Generate offer ID
let offer_id = 1u64;

// Derive PDA for offer account using anchor-litesvm helper
let (offer_account, _offer_bump) = ctx
.svm
.get_pda_with_bump(&[b"offer", &offer_id.to_le_bytes()], &program_id);

// Calculate associated token address for vault
let vault = spl_associated_token_account::get_associated_token_address(
&offer_account,
&token_mint_a.pubkey(),
);

// Build make_offer instruction data manually
// Anchor discriminator (8 bytes) + offer_id (8 bytes) + token_a_offered_amount (8 bytes) + token_b_wanted_amount (8 bytes)
let discriminator_input = b"global:make_offer";
let discriminator = anchor_lang::solana_program::hash::hash(discriminator_input).to_bytes()[..8].to_vec();

let mut instruction_data = discriminator;
instruction_data.extend_from_slice(&offer_id.to_le_bytes());
instruction_data.extend_from_slice(&(1 * TOKEN_A).to_le_bytes());
instruction_data.extend_from_slice(&(1 * TOKEN_B).to_le_bytes());

// Build account metas
let account_metas = vec![
AccountMeta::new_readonly(spl_associated_token_account::ID, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(anchor_lang::system_program::ID, false),
AccountMeta::new(alice.pubkey(), true),
AccountMeta::new_readonly(token_mint_a.pubkey(), false),
AccountMeta::new_readonly(token_mint_b.pubkey(), false),
AccountMeta::new(alice_token_account_a, false),
AccountMeta::new(offer_account, false),
AccountMeta::new(vault, false),
];

let make_offer_ix = Instruction {
program_id,
accounts: account_metas,
data: instruction_data,
};

// Execute instruction and assert success using anchor-litesvm helpers
let result = ctx.execute_instruction(make_offer_ix, &[&alice]).unwrap();
result.assert_success();
}
}

To have our new test run with cargo test, you need to register the test module in lib.rs so it’s included by the crate’s test harness:

#[cfg(test)]
mod anchor_tests;

Rerun the tests and look for:

test anchor_tests::tests::anchor_test_make_offer_succeeds ... ok

Now that you understand how anchor-litesvm reduces the need for boilerplate, try rewriting the other tests for a deeper understanding.

Wrapping Up

Nice work getting this far! You’ve seen how to run a full Solana program entirely in-process with LiteSVM, set up a reusable test harness, load a compiled Anchor program, build out account and token state on the fly, and verify real instruction flows without ever starting a separate validator. You also got a preview of how anchor-litesvm reduces boilerplate so you can focus on your program's logic instead of wiring.

From here, you’re in a great position to extend these tests, add new scenarios, and use LiteSVM as a core part of how you ship safer Solana programs faster.

Resources


We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide