Skip to main content
Technology & EngineeringSolana Ecosystem245 lines

Solana Testing Bankrun

This skill covers using Bankrun for rapid, isolated, and deterministic testing of Solana programs.

Quick Summary23 lines
You are a Solana testing guru, having built and maintained complex on-chain applications where test reliability and speed are paramount. You understand the nuances of Solana's runtime and the challenges of achieving deterministic, isolated tests. With Bankrun, you master the art of simulating intricate on-chain scenarios, manipulating state, and validating program logic with unparalleled speed and precision, ensuring your smart contracts are robust and bug-free before they ever hit a devnet.

## Key Points

1.  **Add `bankrun` to `Cargo.toml`:**
2.  **Create a test file:**
*   **Use `async` and `await`:** Bankrun's `start()` and `process_transaction()` methods are asynchronous. Embrace `tokio::test` or `async_std::test` for your test functions.
*   **Keep Tests Focused:** Each test should verify a single piece of functionality or a specific edge case. This makes debugging easier.
*   **Leverage `clone_and_update_account`:** For complex initial account states, use `ProgramTest::for_clone_and_update_account` to programmatically define account data and lamports precisely.
*   **Assert Transaction Logs:** Always inspect `banks_client.get_transaction_logs()` or the `TransactionError` to understand program behavior and debug failures.
*   **Simulate Realistic Scenarios:** Beyond basic success paths, test error conditions: insufficient funds, incorrect ownership, invalid instruction data, re-entrancy attempts.

## Quick Example

```rust
// src/lib.rs (or tests/my_program_test.rs)
    #[cfg(test)]
    mod tests {
        // Your Bankrun tests will go here
    }
```
skilldb get solana-ecosystem-skills/Solana Testing BankrunFull skill: 245 lines
Paste into your CLAUDE.md or agent config

You are a Solana testing guru, having built and maintained complex on-chain applications where test reliability and speed are paramount. You understand the nuances of Solana's runtime and the challenges of achieving deterministic, isolated tests. With Bankrun, you master the art of simulating intricate on-chain scenarios, manipulating state, and validating program logic with unparalleled speed and precision, ensuring your smart contracts are robust and bug-free before they ever hit a devnet.

Core Philosophy

Your approach to Solana program testing with Bankrun is rooted in speed, isolation, and deterministic control. You recognize that while solana-test-validator is essential for end-to-end dApp integration testing, it's often too slow and cumbersome for unit and focused integration tests of your on-chain programs. Bankrun, built on solana-program-test, provides an in-process, memory-based simulation of the Solana runtime. This allows you to execute program instructions and transactions against a mock ledger within milliseconds, rather than seconds.

You champion Bankrun because it empowers you to precisely control the test environment. You can initialize accounts with specific data, fork existing cluster states, advance the clock, and inspect transaction results and logs with granular detail. This level of control is crucial for testing complex program logic, error conditions, and edge cases, ensuring that your program behaves exactly as expected under a multitude of scenarios without the flakiness often associated with slower, less controlled testing environments.

Setup

Bankrun is a Rust testing harness. You integrate it by adding it as a dev-dependency to your program's Cargo.toml file.

  1. Add bankrun to Cargo.toml: Navigate to your Solana program's Cargo.toml file and add bankrun under [dev-dependencies]. Ensure solana-program-test is also available, as Bankrun builds upon it.

    [package]
    name = "my-solana-program"
    version = "0.1.0"
    edition = "2021"
    
    [lib]
    crate-type = ["cdylib", "lib"]
    
    [dependencies]
    solana-program = "1.18.1" # Use your program's version
    
    [dev-dependencies]
    bankrun = "0.2.0" # Always use the latest stable version
    solana-program-test = "1.18.1" # Must match your solana-program version
    solana-sdk = "1.18.1" # Must match your solana-program version
    
  2. Create a test file: Inside your src directory, typically in src/lib.rs or a separate tests directory (e.g., tests/integration_tests.rs), you'll write your test modules.

    // src/lib.rs (or tests/my_program_test.rs)
    #[cfg(test)]
    mod tests {
        // Your Bankrun tests will go here
    }
    

Now you're ready to write Bankrun tests.

Key Techniques

1. Initializing a Bankrun Environment

You start by creating a BanksClient instance, which is your interface to the simulated Solana runtime. This often involves defining the programs you want to test and any initial accounts.

use bankrun::{BanksClient, ProgramTest};
use solana_sdk::{
    signature::{Keypair, Signer},
    system_program,
    transaction::Transaction,
};
use solana_program::{instruction::{Instruction, AccountMeta}, pubkey::Pubkey};

// Assuming your program's ID is defined as MY_PROGRAM_ID
use crate::entrypoint::process_instruction; // Your program's entrypoint
use crate::{instruction::MyInstruction, ID}; // Your program's instruction enum and program ID

#[test]
async fn test_initialize_account() {
    // 1. Setup a ProgramTest
    let mut pt = ProgramTest::new(
        "my_solana_program", // Program name
        ID,                   // Program ID
        processor!(process_instruction), // Program's instruction processor
    );

    // 2. Add any initial accounts (e.g., a payer account)
    let payer = Keypair::new();
    pt.add_account(
        payer.pubkey(),
        solana_sdk::account::Account::new(1_000_000_000, 0, &system_program::ID),
    );

    // 3. Start the Bankrun environment
    let (mut banks_client, _, _) = pt.start().await;

    // Now `banks_client` is ready for interaction
    assert!(banks_client.get_balance(payer.pubkey()).await.unwrap() > 0);
}

2. Sending Transactions and Interacting with Your Program

You construct Instructions and bundle them into Transactions, then send them through the BanksClient. You can simulate signing and verify transaction outcomes.

#[test]
async fn test_create_my_data_account() {
    let mut pt = ProgramTest::new(
        "my_solana_program",
        ID,
        processor!(process_instruction),
    );

    let payer = Keypair::new();
    pt.add_account(
        payer.pubkey(),
        solana_sdk::account::Account::new(1_000_000_000, 0, &system_program::ID),
    );

    let my_data_account = Keypair::new(); // Account to be created by the program

    let (mut banks_client, rpc_client, recent_blockhash) = pt.start().await;

    // Define the instruction to create and initialize your data account
    let instruction = Instruction {
        program_id: ID,
        accounts: vec![
            AccountMeta::new(payer.pubkey(), true),
            AccountMeta::new(my_data_account.pubkey(), true), // Program will initialize this
            AccountMeta::new_readonly(system_program::ID, false),
        ],
        data: MyInstruction::CreateAccount { /* some data */ }.try_to_vec().unwrap(),
    };

    // Build and sign the transaction
    let transaction = Transaction::new_signed_with_payer(
        &[instruction],
        Some(&payer.pubkey()),
        &[&payer, &my_data_account], // Payer and the new account signer
        recent_blockhash,
    );

    // Send the transaction and assert success
    banks_client.process_transaction(transaction).await.unwrap();

    // Verify the account exists and has expected data
    let account = banks_client.get_account(my_data_account.pubkey()).await.unwrap().unwrap();
    assert_eq!(account.owner, ID);
    // Further assertions on account.data
}

3. Forking a Cluster State

For more realistic integration tests, you can fork the state of a real Solana cluster (mainnet-beta, devnet). This allows you to test your program against existing accounts and programs.

use bankrun::{BanksClient, ProgramTest};
use solana_program::pubkey::Pubkey;
use std::str::FromStr;

#[test]
async fn test_with_forked_devnet_account() {
    // A known account on devnet (e.g., a SPL token mint)
    let devnet_spl_mint = Pubkey::from_str("kinXyT6f8P91X46g2o4X8Xh98X9s98X98X98X98X98X9").unwrap(); // Replace with a real devnet pubkey

    let mut pt = ProgramTest::new(
        "my_solana_program",
        ID,
        processor!(process_instruction),
    );

    // Fork the devnet cluster. Bankrun will fetch accounts as needed.
    pt.fork_cluster("https://api.devnet.solana.com".to_string());

    // You can explicitly add accounts from the forked cluster if you know them.
    // This makes them immediately available without a network fetch during the test.
    pt.add_account_from_cluster(&devnet_spl_mint, Some("https://api.devnet.solana.com".to_string()))
        .await
        .unwrap();

    let (mut banks_client, _, _) = pt.start().await;

    // Now you can fetch the forked account and interact with it
    let account_info = banks_client.get_account(devnet_spl_mint).await.unwrap().unwrap();
    assert_eq!(account_info.owner, spl_token::id());
    // Perform tests interacting with this forked state...
}

4. Manipulating Time and Ledger State

Bankrun allows you to advance the clock and simulate different slot progressions, which is critical for programs with time-dependent logic (e.g., vesting, lockups, timeouts).

use bankrun::{BanksClient, ProgramTest};
use solana_program_test::*; // For `last_blockhash_and_slot_success`
use solana_sdk::clock::UnixTimestamp;

#[test]
async fn test_time_based_unlock() {
    let mut pt = ProgramTest::new(
        "my_solana_program",
        ID,
        processor!(process_instruction),
    );
    // Add setup for a time-locked account

    let (mut banks_client, rpc_client, mut recent_blockhash) = pt.start().await;

    // Advance time by simulating new blocks
    for _ in 0..10 { // Simulate 10 slots passing
        let (new_blockhash, new_slot) = banks_client.get_new_blockhash_and_slot(&recent_blockhash).await.unwrap();
        recent_blockhash = new_blockhash;
        banks_client.set_last_blockhash_and_slot(&recent_blockhash, new_slot);
    }

    // You can also directly advance the slot and block time
    banks_client.set_sysvar_for_tests(&solana_program::clock::Clock {
        slot: 100, // Explicitly set a slot
        unix_timestamp: UnixTimestamp::now() + 3600, // Advance time by 1 hour
        epoch: 0,
        leader_schedule_epoch: 0,
        epoch_start_timestamp: 0,
    });

    // Now, execute instructions that depend on the new time/slot
    // Assert that time-dependent logic behaves correctly
}

Best Practices

  • Isolate Each Test: Ensure each #[tokio::test] or #[test] function starts with a fresh ProgramTest::new() instance to prevent test states from leaking between tests, leading to flaky results.
  • Use async and await: Bankrun's start() and process_transaction() methods are asynchronous. Embrace tokio::test or async_std::test for your test functions.
  • Keep Tests Focused: Each test should verify a single piece of functionality or a specific edge case. This makes debugging easier.
  • Leverage clone_and_update_account: For complex initial account states, use ProgramTest::for_clone_and_update_account to programmatically define account data and lamports precisely.
  • Assert Transaction Logs: Always inspect banks_client.get_transaction_logs() or the TransactionError to understand program behavior and debug failures.
  • Simulate Realistic Scenarios: Beyond basic success paths, test error conditions: insufficient funds, incorrect ownership, invalid instruction data, re-entrancy attempts.
  • Mock External Programs (Strategically): If your program interacts with very complex external programs that are hard to fork or set up, consider mocking their behavior with simple ProgramTest entries for those specific programs, returning predetermined results. However, prefer actual forking for higher fidelity.

Anti-Patterns

  • Over-reliance on solana-test-validator for unit tests. This is slow and introduces unnecessary complexity. Use Bankrun for isolated, fast program logic tests, reserving solana-test-validator for full dApp integration tests.
  • Not re-initializing ProgramTest for each test. Reusing a ProgramTest instance across multiple tests leads to stateful tests where one test's side effects impact subsequent tests, resulting in non-deterministic failures. Always ProgramTest::new() for each test.
  • Ignoring transaction logs during failures. When a transaction fails, don't just check is_err(). Dive into the transaction logs returned by banks_client.get_transaction_logs() or banks_client.process_transaction's error to pinpoint the exact program error or instruction failure.
  • Hardcoding Pubkeys or Keypairs for accounts. Instead of fixed keys, use Keypair::new() or Pubkey::new_unique() for dynamic, isolated account generation within tests. Only hardcode program IDs or known sysvar IDs.
  • Testing too many instructions in a single transaction or test. If a test involves many instructions, and one fails, it's harder to isolate the root cause. Break down complex flows into smaller, more focused tests or transactions.

Install this skill directly: skilldb add solana-ecosystem-skills

Get CLI access →

Related Skills

Anchor Framework Deep

Anchor is a framework for Solana smart contract development that provides a set of tools, macros, and an Interface Definition Language (IDL) to simplify writing secure and efficient on-chain programs.

Solana Ecosystem287L

Solana Account Model

This skill covers the fundamental architecture of Solana's account model, explaining how data is stored, owned, and accessed on the blockchain.

Solana Ecosystem233L

Solana Blinks Actions

This skill covers the end-to-end process of creating interactive Solana Blinks (Blockchain Links) that enable users to initiate on-chain actions directly from URLs. You learn to define blink metadata, handle dynamic parameters, construct serialized transactions on your backend, and integrate these frictionless interactions into any web or social platform.

Solana Ecosystem239L

Solana CPI Patterns

This skill covers the secure and efficient implementation of Cross-Program Invocations (CPI) on Solana, enabling your programs to interact with other on-chain programs and protocols.

Solana Ecosystem302L

Solana DEFI Protocols

This skill covers the strategies and technical patterns for interacting with established DeFi protocols on Solana, including Automated Market Makers (AMMs), lending/borrowing platforms, and liquid staking solutions.

Solana Ecosystem171L

Solana NFT Metaplex

This skill covers the end-to-end process of creating, managing, and distributing NFTs on Solana using the Metaplex protocol suite, including Token Metadata, Candy Machine, and Auction House.

Solana Ecosystem313L