Skip to content

Testing & Localnet

Thorough testing before deploying to a live cluster prevents costly bugs and failed transactions. Zink supports the same testing tools and workflows as Solana — from fast in-process unit tests to full integration tests against a local validator or testnet.

Testing strategies overview

StrategySpeedFidelityBest for
Unit tests (Mollusk, litesvm)FastestSimulated runtimeInstruction logic, edge cases
solana-program-testFastIn-process BPF VMProgram integration without a validator
solana-test-validatorModerateFull validatorEnd-to-end with real RPC, wallets, and CPI
Zink TestnetSlowProduction-likePre-production validation, network-specific behavior

Use all four layers. Fast tests catch logic errors early; slower tests catch integration and environment issues.

Unit testing with Mollusk

Mollusk is a lightweight harness for testing individual Solana program instructions without spinning up a validator. It executes your program's instruction processor directly in native Rust and works well with Starframe-generated instruction builders.

toml
# Cargo.toml [dev-dependencies]
mollusk-svm = "0.7"
solana-account = "3"
rust
use mollusk_svm::{result::Check, Mollusk};
use solana_account::Account as SolanaAccount;
use star_frame::{client::MakeInstruction, prelude::Pubkey};

#[test]
fn test_initialize() -> Result<(), Box<dyn std::error::Error>> {
    let mollusk = Mollusk::new(&MyProgram::ID, "my_program");
    let authority = Pubkey::new_unique();
    let counter = Pubkey::new_unique();

    let ix = MyProgram::instruction(
        &InitializeCounter { start_at: Some(42) },
        InitializeClientAccounts {
            authority,
            counter,
            system_program: None,
        },
    )?;

    mollusk.process_and_validate_instruction(
        &ix,
        &[
            Check::success(),
            Check::account(&counter).build(),
        ],
    );

    Ok(())
}

Mollusk is the fastest way to iterate on instruction logic. It skips transaction wrapping, signature verification, and bank processing — just your instruction in, result out.

Unit testing with litesvm

litesvm provides a lightweight SVM simulator that supports more runtime features than Mollusk (sysvars, CPI, program deployment) while remaining faster than solana-program-test:

toml
# Cargo.toml [dev-dependencies]
litesvm = "0.3"
rust
use litesvm::LiteSVM;
use solana_sdk::{
    signature::Keypair, signer::Signer, system_instruction,
    transaction::Transaction,
};

#[test]
fn test_transfer() {
    let mut svm = LiteSVM::new();
    let payer = Keypair::new();
    svm.airdrop(&payer.pubkey(), 10_000_000_000).unwrap();

    let recipient = Keypair::new();
    let ix = system_instruction::transfer(&payer.pubkey(), &recipient.pubkey(), 1_000_000);

    let blockhash = svm.latest_blockhash();
    let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash);

    svm.send_transaction(tx).unwrap();
    assert_eq!(svm.get_balance(&recipient.pubkey()).unwrap(), 1_000_000);
}

litesvm supports deploying programs, setting sysvars, and simulating CPI — making it a good middle ground between Mollusk and a full validator.

Integration testing with solana-program-test

The solana-program-test crate runs an in-process BPF runtime. It is more faithful than Mollusk/litesvm but slower, and supports the full range of runtime features including CPI, sysvars, and program deployment.

toml
# Cargo.toml [dev-dependencies]
solana-program-test = "1.18"
solana-sdk = "1.18"
rust
use solana_program_test::*;
use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction};

#[tokio::test]
async fn test_initialize() {
    let program_id = Pubkey::new_unique();
    let mut context = ProgramTest::new("my_program", program_id, None)
        .start_with_context()
        .await;

    let account = Keypair::new();
    let tx = Transaction::new_signed_with_payer(
        &[/* instructions */],
        Some(&context.payer.pubkey()),
        &[&context.payer, &account],
        context.last_blockhash,
    );

    context.banks_client.process_transaction(tx).await.unwrap();
}

Starframe scaffold tests

Projects created with sf new include a Rust test module under src/tests/. The default counter test initializes a PDA, increments it, and checks the serialized account state with Mollusk.

bash
# Fast native tests
cargo test

# Build and test the deployable binary
cargo build-sbf
cargo test-sbf

# Generate a Codama-compatible IDL from the test-only idl feature
cargo test --features idl -- generate_idl

The generated test intentionally skips the Mollusk program test if target/deploy/<program>.so is missing, which keeps plain cargo test fast while cargo build-sbf && cargo test-sbf validates the compiled artifact. If your toolchain does not include cargo test-sbf, rerun cargo test after cargo build-sbf; the test also checks SBF_OUT_DIR and target/deploy.

Running solana-test-validator

The local test validator provides a full single-node cluster on your machine:

bash
solana-test-validator

Default settings:

PropertyValue
RPC URLhttp://127.0.0.1:8899
WebSocketws://127.0.0.1:8900
Faucethttp://127.0.0.1:9900
Ledger./test-ledger/

Pre-loading programs and accounts

You can start the validator with specific programs or accounts already loaded — useful for testing against deployed programs or other ecosystem dependencies:

bash
# Load a compiled program at a specific address
solana-test-validator \
  --bpf-program <PROGRAM_ID> target/deploy/my_program.so

# Clone an account from a live cluster (e.g., Zink mainnet)
solana-test-validator \
  --clone <ACCOUNT_ADDRESS> \
  --url https://testnet-rpc.z.ink

# Clone a program from a live cluster
solana-test-validator \
  --clone-upgradeable-program <PROGRAM_ID> \
  --url https://testnet-rpc.z.ink

Zink recommendation

When testing programs that interact with existing Zink infrastructure, clone the relevant program accounts from Zink to your local validator. This ensures your tests run against the same program versions deployed in production. Coordinate with the relevant maintainers for the set of program IDs to clone.

Resetting state

bash
# Wipe the ledger and start fresh
solana-test-validator --reset

Configuring the validator to mirror Zink

To make the local environment closer to Zink production configuration:

bash
solana-test-validator --reset

When a test depends on Zink-specific timing, pass the relevant validator flags explicitly instead of assuming upstream defaults are close enough. For ordinary instruction and integration tests, keep the local validator simple.

Zink-specific

Use local timing overrides only when you are validating behavior that is sensitive to slot or epoch cadence. For most program tests, the default solana-test-validator timing model is sufficient.

Testing against Zink Testnet

For pre-production validation, deploy your program to Zink Testnet and run your integration or client tests against that RPC endpoint:

bash
# Configure CLI for Zink Testnet
solana config set --url https://testnet-rpc.z.ink
solana balance

# Build and deploy
cargo build-sbf
solana program deploy \
  target/deploy/my_program.so \
  --program-id target/deploy/my_program-keypair.json

# Run your RPC-facing integration tests against Zink
ZINK_RPC_URL=https://testnet-rpc.z.ink cargo test --test integration -- --ignored

Testnet testing is essential for validating behavior that differs between local and live environments: real slot timing, actual fee calculation, and interaction with other deployed programs.

Zink-specific

The docs currently publish the Zink Testnet RPC endpoint, not a generic faucet guarantee. Make sure the wallet you use here is funded through the workflow for your environment before you try to deploy.

Debugging failed transactions

Reading program logs

Every program instruction can emit logs via msg!(). View them with:

bash
# Fetch logs for a specific transaction
solana confirm -v <TRANSACTION_SIGNATURE>

# Stream logs in real time from the local validator
solana logs

Simulating transactions

Before sending a transaction, simulate it to see if it would succeed:

bash
solana transaction-history <ADDRESS> --show-transactions

In TypeScript:

typescript
const simulation = await connection.simulateTransaction(transaction);
console.log(simulation.value.logs);

Simulation returns program logs and the compute units consumed — useful for budgeting and debugging.

Common error patterns

ErrorLikely cause
AccountNotFoundThe account doesn't exist or wasn't created before the instruction ran
InsufficientFundsNot enough lamports to cover rent-exemption or transfer
PrivilegeEscalationAttempting to write to an account your program doesn't own
ProgramFailedToCompleteRan out of compute units — increase the compute budget
AccountAlreadyInitializedTrying to init an account that already has data

CI integration

Run your test suite in CI with a local validator and the Solana toolchain:

yaml
# GitHub Actions example
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: solana-labs/solana-install@v1
        with:
          solana-version: "1.18"
      - run: cargo build
      - run: cargo test
      - run: cargo build-sbf
      - run: cargo test-sbf

For Starframe scaffold tests, keep the post-cargo build-sbf SBF test step so CI validates the compiled .so path as well as the native test path. Use cargo test as the fallback only when the installed Solana toolchain does not provide cargo test-sbf.

For RPC-facing integration tests, start solana-test-validator in CI or point the test suite at a funded Zink Testnet wallet and endpoint.

Next steps

  • Deployment — deploy your tested program to Zink Testnet or mainnet
  • Programs — how the SVM executes your program bytecode
  • Fees — understanding compute budgets for test configuration

Zink is a general-purpose SVM network for programs, operators, and bridge integrations.