Solana Testing Bankrun
This skill covers using Bankrun for rapid, isolated, and deterministic testing of Solana programs. You leverage Bankrun when you need to unit test or integration test your Solana programs locally without the overhead of `solana-test-validator`, enabling faster development cycles and more reliable simulations.
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: 244 linesYou 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.
-
Add
bankruntoCargo.toml: Navigate to your Solana program'sCargo.tomlfile and addbankrununder[dev-dependencies]. Ensuresolana-program-testis 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 -
Create a test file: Inside your
srcdirectory, typically insrc/lib.rsor a separatetestsdirectory (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 freshProgramTest::new()instance to prevent test states from leaking between tests, leading to flaky results. - Use
asyncandawait: Bankrun'sstart()andprocess_transaction()methods are asynchronous. Embracetokio::testorasync_std::testfor 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, useProgramTest::for_clone_and_update_accountto programmatically define account data and lamports precisely. - Assert Transaction Logs: Always inspect
banks_client.get_transaction_logs()or theTransactionErrorto 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
ProgramTestentries for those specific programs, returning predetermined results. However, prefer actual forking for higher fidelity.
Anti-Patterns
- Over-reliance on
solana-test-validatorfor unit tests. This is slow and introduces unnecessary complexity. Use Bankrun for isolated, fast program logic tests, reservingsolana-test-validatorfor full dApp integration tests. - Not re-initializing
ProgramTestfor each test. Reusing aProgramTestinstance across multiple tests leads to stateful tests where one test's side effects impact subsequent tests, resulting in non-deterministic failures. AlwaysProgramTest::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 bybanks_client.get_transaction_logs()orbanks_client.process_transaction's error to pinpoint the exact program error or instruction failure. - Hardcoding
Pubkeys orKeypairs for accounts. Instead of fixed keys, useKeypair::new()orPubkey::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
Related Skills
Solana Token Program
The Solana Token Program (SPL Token program) is the official on-chain program for creating, managing, and transferring fungible and non-fungible tokens on Solana. You leverage this skill to implement custom token economies, issue NFTs, manage asset custody, and build decentralized applications that interact with standardized digital assets.
Solana Transaction Optimization
This skill focuses on strategies and techniques to reduce transaction costs, increase transaction reliability, and improve user experience on Solana by optimizing transaction size, compute unit consumption, and priority fee application. You leverage these optimization methods when building high-throughput Solana dApps, interacting with congested programs, or ensuring predictable and affordable transaction execution for your users.
Solana Validator Setup
This skill guides you through the process of setting up and operating a Solana validator, enabling you to participate in network consensus, process transactions, and earn staking rewards. You leverage this skill when contributing to the decentralization and security of the Solana network, ensuring high availability and performance for your validator node.
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. You leverage Anchor when building complex Solana dApps, needing robust account validation, streamlined testing, and a clear client-side interface.
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. You leverage this understanding to design efficient data structures, manage on-chain state, and interact with programs, forming the bedrock for any Solana dApp development.
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. You leverage Blinks to significantly enhance user experience by streamlining dApp interactions, reducing onboarding friction, and allowing users to engage with your dApp's core functionalities with a single click, without requiring a full UI load.