How to test a Solana Program using Vitest & LiteSVM
Fast, lightweight unit testing without solana-test-validator or heavy dependencies

Frontend Engineer & Technical Writer building scalable, user-focused web applications with TypeScript, React, and Next.js. I enjoy turning complex ideas into clean interfaces and clear documentation. Currently exploring blockchain development with Rust on the Solana network, focusing on smart contracts and decentralised applications.
Introduction
Testing your Solana program before deployment isn't just good practice. It's essential to ensure it works as intended and won't fail when real value is at stake.
However, traditional testing methods that rely on the solana test validator often make this step a bottleneck. They are slow, resource-heavy, and difficult to reproduce in a Continuous Integration(CI) environment.
Here's how switching to LiteSVM and Vitest can slash your Solana test times from seconds to milliseconds.
Prerequisites
As part of the prerequisites, you should have Anchor installed on your pc. You can look at the quick installation guide.
You should have some experience with JavaScript/TypeScript, and some knowledge about Solana programs, accounts and transactions will be good.
Here are the versions of dependencies on my pc so you can follow along.
Installed Versions
rustc 1.93.1 (01f6ddf75 2026-02-11)
solana-cli 2.2.20 (src:dabc99a5; feat:3073396398, client:Agave)
anchor-cli 0.32.1
node.js v24.10.0
Project Setup and Configurations
This is the program we will be testing. It collects bio-data from the user and stores it on the blockchain.
use anchor_lang::prelude::*;
declare_id!("YOUR_PROGRAM_ID");
// Anchor adds an 8-byte label to everything we store.
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;
// Program macro
#[program]
// rust program
pub mod profile {
// use what we imported from anchor
use super::*;
// pass in arguments for context
pub fn set_profile(
context: Context<SetProfile>, full_name: String, bio: String, years_of_experience: u64, portfolio: String, skills: Vec<String>) -> Result<()> {
// set profile function body
msg!("Greetings from {}", context.program_id); //the message macro to print out a message to the user
let user_public_key = context.accounts.user.key();
msg!("User {}, is a {}, with {} years on experience. skills include {:?}. You can visit their portfolio to learn more {}",
full_name,
bio,
years_of_experience,
skills,
portfolio
); // format message
//Write the information into the profile account provided.
context.accounts.profile.set_inner( Profile {
full_name,
bio,
years_of_experience,
skills,
portfolio
});
Ok(())
}
}
// Marks this struct as a storable blockchain account
#[account]
// Ensures you allocate enough space when creating accounts
#[derive(InitSpace)]
// Defines the data fields for user profiles
pub struct Profile {
#[max_len(40)]
pub full_name: String,
#[max_len(80)]
pub bio: String,
pub years_of_experience: u64,
#[max_len(40)]
pub portfolio: String,
#[max_len(5, 50)]
pub skills: Vec<String>,
}
#[derive(Accounts)]
pub struct SetProfile<'info> {
// Creates a container named SetProfile that holds all accounts needed for the set_profile instruction.
// 'info indicates that these items will live for the lifetime of a Solana account info object
#[account(mut)] // set signer to mutable because they will pay to create a profile on the blockchain
pub user: Signer<'info>, // the transaction must be signed by this account
#[account(
init_if_needed, // create an account if it does not exist
payer = user, // specify the payer as the person who signs to create an account on the blockchain
space = ANCHOR_DISCRIMINATOR_SIZE + Profile::INIT_SPACE, // specify the space(every account has about 8bits + profile struct)
seeds = [b"profile", user.key().as_ref()], // seed as the text 'profile' and the user's public key. (Seeds are what are used to give the account an address; it is a program-derived address.)
bump // Finds a valid Solana address
)]
// The new account is being created with the seeds and allocated space required
pub profile: Account<'info, Profile>,
pub system_program: Program<'info, System>, // Passes a reference to Solana's built-in System Program (needed to create new accounts).
}
1. Initial Setup
Run the command below and follow the prompts to create a Solana Decentralised Application (dApp) on your computer.
npm create solana-dapp@latest
# Enter your project name
# Select `Kit Framework`
# Choose `next-anchor` template
# Wait for dependcies to install ☕
2. Program Structure Updates
Rename the program directory.
# Rename folder from `vault` to `profile`
mv anchor/programs/vault anchor/programs/profile
Update program code:
Replace
lib.rscontent with the Solana program codeImportant: Copy your program ID into the program code
Clean Up
# Remove the default test file
rm anchor/programs/profile/src/tests.rs
Update Cargo.toml file:
Located at anchor/programs/profile/Cargo.toml, update:
# Update references from 'vault' to 'profile'
[package]
name = "profile"
[features]
name = "profile"
3. Anchor Configuration
Replace Anchor.toml with the code below. Be sure to replace the placeholder text with your program address.
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.devnet]
profile = "YOUR_PROGRAM_ADDRESS"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cd .. && npx vitest run anchor/tests"
4. Test Setup
Create the test structure.
mkdir anchor/tests/fixtures
touch anchor/tests/profile.spec.ts
5. Install Test Dependencies
Install liteSVM and Vitest as dev dependencies.
npm install --save-dev litesvm vitest @solana/web3.js @coral-xyz/anchor
6. Create vitest.config.js file
In the root directory of your project, create a vitest.config.ts file and paste the code below.
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
testTimeout: 50000,
},
});
Add the code snippet below to your tsconfig.json file, inside the compilerOptions object.
"types": ["vitest/globals", "node"],
Add the test command to your package.json file, and paste the key-value pair in the script object.
"test": "cd anchor && vitest run",
What is LiteSVM
LiteSVM is a lightweight Solana Virtual Machine that runs in the test process. It embeds the Solana runtime directly in your tests so you don’t need to spin up a separate validator instance.
Why use LiteSVM
It is the modern choice for Solana developers, aside from speed, which is an undeniable feature. Here are a few reasons to choose LiteSVM over the Solana test validator.
| Traditional Approach (Solana test validator) | LiteSVM Approach |
|---|---|
| Starts a separate validator instance | Runs in the test process |
| Slow startup and breakdown between tests | Executes tests in milliseconds |
| Resource-heavy and memory-intensive | Lightweight and efficient |
| Difficult to reproduce in CI environments | Portable and consistent everywhere |
| Requires network communication | No external dependencies |
Writing the Test File: A Comprehensive Guide
Now that we have our project configured, we can start writing our test suites.
Open anchor/tests/profile.spec.ts, create the file if it does not exist. Okay, now the fun part begins.
We will write two simple but powerful tests with vitest and litesvm, which makes it super fast.
Can create/set a profileCan update existing profile
Step 1: The Imports
At the top of your test file, paste these imports.
import { describe, it, expect, beforeEach } from "vitest";
import { LiteSVM } from "litesvm";
import {
PublicKey,
Transaction,
SystemProgram,
Keypair,
LAMPORTS_PER_SOL,
TransactionInstruction,
} from "@solana/web3.js";
import { Program, BN } from "@coral-xyz/anchor";
import profileIdl from "../target/idl/profile.json";
import fs from "fs";
import path from "path";
Let’s briefly explain what is going on here, so you can understand what we will be using the imports for.
Vitest utilities (
describe,it,expect,beforeEach):
If you have worked with Jest before, you should be familiar with these concepts.-
describe→ This helps organise the test suite.
-it→ Defines individual test cases.
-expect→ It is used to check the outcome of a test case.
-beforeEach→ It runs a setup code before every test.LiteSVM: With this import, we can access an in-memory Solana runtime.Solana JS SDK (
PublicKey,Transaction,SystemProgram,Keypair,LAMPORTS_PER_SOL,TransactionInstruction):
For interactions with the blockchain, we will go into details on the functions used.Anchor framework(
Program,BN):
This imports the anchor framework for Solana programs.
-Program→ Creates a program client to encode and decode data using your program’s IDL.
-BN→ This is Anchor’s way to handle big number class.profileIdl→ This file is generated after running thebuildcommand. It describes the program's structure.Core node modules (
fs,path) → They provide functions to interact with files on your computer, and transform file paths for different operating systems.
Step 2: Define the Program ID
Every Solana program has a unique public address. We use this address to tell our test which program to interact with.
After the imports, add the code snippet below and replace the placeholder text with the address from the lib.rs file.
const programId = new PublicKey("YOUR_PROGRAM_ID");
Step 3: The Main Test Suite
Now we create a structure for our test suite to describe how we want it to behave when we run a test.
describe("profile program", () => {
let svm: LiteSVM;
let user: Keypair;
let profilePda: PublicKey;
let program: Program;
beforeEach(() => {
// Creates a fresh solana vm instance for every test
svm = new LiteSVM();
// Load your compiled solana program binary
const programBytes = fs.readFileSync(
path.resolve(__dirname, "../target/deploy/profile.so")
);
svm.addProgram(programId, programBytes);
// Create a new user who will sign transactions
user = Keypair.generate();
// Give them some SOL to pay for creating an account
svm.airdrop(user.publicKey, BigInt(10 * LAMPORTS_PER_SOL));
// Calculate the PDA exactly like in your Rust code
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("profile"), user.publicKey.toBuffer()],
programId
);
profilePda = pda;
// Create an Anchor Program instance just for encoding/decoding
program = new Program(profileIdl, programId);
});
// ← our two tests will go here
});
Let's break it down. Whenever we run a test, we want to;
Create a new Solana blockchain instance.
Read the compiled Solana program.
Add the loaded Solana program into the VM.
Generate a new public address (wallet) for testing.
Airdrop some fake SOL to the wallet for transaction payments.
Calculate the Program Derived Address (PDA) where our data will be stored on the blockchain.
Create a Program instance to decode instructions for our program.
Step 4: The First Test - Create a Profile
Now, let's replicate the logic to create a profile in our test file. Add the code below the describe block.
it("can create / set a profile", () => {
const fullName = "Haruna Alvin";
const bio = "Solana dev from Lagos building dApps";
const yearsOfExperience = new BN(6);
const portfolio = "https://harunadev.netlify.app";
const skills = ["Rust", "Anchor", "Next.js", "Solana"];
// Step 1: Turn our nice JS object into raw Borsh bytes (what Rust expects)
const ixData = program.coder.instruction.encode("setProfile", {
fullName,
bio,
yearsOfExperience,
portfolio,
skills,
});
// Step 2: Build the instruction with the correct accounts
const ix = new TransactionInstruction({
programId,
keys: [
{ pubkey: user.publicKey, isSigner: true, isWritable: true }, // payer + signer
{ pubkey: profilePda, isSigner: false, isWritable: true }, // the profile we'll create/update
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data: ixData,
});
// Step 3: Build a real transaction
const blockhash = svm.latestBlockhash();
const tx = new Transaction();
tx.recentBlockhash = blockhash;
tx.feePayer = user.publicKey;
tx.add(ix);
tx.sign(user);
// Step 4: Execute it inside LiteSVM
svm.sendTransaction(tx);
// Step 5: Fetch the account and decode it
const accountInfo = svm.getAccount(profilePda);
if (!accountInfo) {
throw new Error("Profile account not created");
}
const profile = program.coder.accounts.decode(
"profile",
Buffer.from(accountInfo.data)
);
// Step 6: Make sure everything saved correctly
expect(profile.fullName).toBe(fullName);
expect(profile.bio).toBe(bio);
expect(profile.yearsOfExperience.toString()).toBe("6");
expect(profile.portfolio).toBe(portfolio);
expect(profile.skills).toEqual(skills);
});
In our test block, we attempt to replicate the steps of creating a profile similar to the Solana program.
It follows a prepare → execute → verify workflow, to ensure that a user can create a profile on the blockchain.
It prepares the profile information to be stored on the blockchain.
Converts profile information into raw bytes that Solana can understand.
Creates an instruction program with the profile information and an address to store the data.
Wraps the instruction in a block hash with a timestamp and signature.
Sends the transaction in the VM for processing.
Retrieves the stored data from the blockchain using the PDA and converts it to a readable format.
Compares the data from the blockchain with the profile information that was sent.
Step 5: Second Test - Update an Existing Profile
Add the second test block after the first. In this second test, we;
Create a profile on the blockchain
Verify the data stored
Update the profile stored in that same address on the blockchain
Verify the current data stored on the blockchain.
it("can update existing profile", () => {
// First — create the profile (copy-paste from previous test logic)
const createData = program.coder.instruction.encode("setProfile", {
fullName: "Haruna Alvin",
bio: "Solana dev from Lagos building dApps",
yearsOfExperience: new BN(6),
portfolio: "harunadv.netlify.app",
skills: ["Rust", "Anchor", "Next.js", "Solana"],
});
const createIx = new TransactionInstruction({
programId,
keys: [
{ pubkey: user.publicKey, isSigner: true, isWritable: true },
{ pubkey: profilePda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data: createData,
});
let tx = new Transaction();
tx.recentBlockhash = svm.latestBlockhash();
tx.feePayer = user.publicKey;
tx.add(createIx);
tx.sign(user);
svm.sendTransaction(tx);
// Quick safety check
let accountInfo = svm.getAccount(profilePda);
if (!accountInfo) throw new Error("Profile not created for update test");
// Now — update it with new data
const updateData = program.coder.instruction.encode("setProfile", {
fullName: "Haruna Alvin Ojonimi",
bio: "Call of duty gamer",
yearsOfExperience: new BN(15),
portfolio: "codm",
skills: ["Reload", "Drone strike user", "ak-117 expert"],
});
const updateIx = new TransactionInstruction({
programId,
keys: [
{ pubkey: user.publicKey, isSigner: true, isWritable: true },
{ pubkey: profilePda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
data: updateData,
});
tx = new Transaction();
tx.recentBlockhash = svm.latestBlockhash();
tx.feePayer = user.publicKey;
tx.add(updateIx);
tx.sign(user);
svm.sendTransaction(tx);
// Check the final state
accountInfo = svm.getAccount(profilePda);
if (!accountInfo) throw new Error("Profile disappeared after update");
const profile = program.coder.accounts.decode(
"profile",
Buffer.from(accountInfo.data)
);
expect(profile.fullName).toBe("Haruna Alvin Ojonimi");
expect(profile.yearsOfExperience.toString()).toBe("15");
expect(profile.skills).toEqual(["Reload", "Drone strike user", "ak-117 expert"]);
});
Step 6: Build Program and Run Tests
Now our tests are good to run! Back in your terminal (project root), paste the commands below and follow the prompts.
npm run anchor-build
# After the build runs successfully, go to anchor/target/deploy copy the profile.so file and paste it in tests/fixtures
npm run test
If you don't encounter any errors, you should see something similar in your terminal.
RUN v4.0.18 /Users/uss-enterprise/Desktop/Projects/liteSvm
✓ anchor/tests/profile.spec.ts (2 tests) 278ms
✓ profile program (2)
✓ can create / set a profile 213ms
✓ can update existing profile 62ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 15:31:01
Duration 879ms (transform 76ms, setup 0ms, import 354ms, tests 278ms, environment 0ms)
Conclusion
So now, we have a working test script for a Solana program. No need for a test validator, and it’s all done with an in-memory VM.
Testing our Solana programs ensures that our code runs as intended and is of good quality, and LiteSVM provides a fast and reliable way to do so.
If you want to further your knowledge on testing Solana programs, I recommend checking out the Anchor LiteSVM Documentation.
In case you run into any issues while building. Send a screenshot to my Email or Discord, and I’ll be happy to assist you.



