Overview
Bankrun is a fast, lightweight testing framework for Solana programs built on top of the Solana Banks client. It provides an ergonomic API for testing Solana programs without the overhead of running a full validator.
Bankrun is primarily used with Solders (Python) and provides similar functionality to LiteSVM, but with a different API design. For TypeScript and Rust testing, consider using [LiteSVMtesting/litesvm) instead.
Key Features
- Fast Execution - No validator startup time, tests run in-process
- Lightweight - Minimal resource usage compared to
solana-test-validator
- Direct SVM Access - Tests programs directly against the Solana Virtual Machine
- Python Support - Part of the Solders Python library ecosystem
- Time Control - Manipulate clock and slot for testing time-dependent logic
When to Use Bankrun
Bankrun is ideal for:
- Python-based testing - If your testing workflow uses Python
- Fast iteration - When you need quick test feedback loops
- Unit testing - Testing individual program instructions in isolation
- CI/CD pipelines - Faster test execution in automated environments
Installation
Bankrun is included as part of the Solders library:
# Using pip
pip install solders
# Using uv (recommended)
uv add solders
Basic Usage
Here’s a simple example of testing a Solana program transfer:
from solders.bankrun import start
from solders.keypair import Keypair
from solders.system_program import transfer, TransferParams
from solders.transaction import Transaction
# Start the test environment
context = await start([], [])
# Create test keypairs
payer = Keypair()
recipient = Keypair()
# Airdrop SOL to payer
await context.set_account(
payer.pubkey(),
Account(lamports=1_000_000_000, owner=SYSTEM_PROGRAM_ID)
)
# Create transfer instruction
instruction = transfer(
TransferParams(
from_pubkey=payer.pubkey(),
to_pubkey=recipient.pubkey(),
lamports=100_000,
)
)
# Build and process transaction
tx = Transaction.new_with_payer([instruction], payer.pubkey())
await context.process_transaction(tx, [payer])
# Verify results
recipient_account = await context.get_account(recipient.pubkey())
assert recipient_account.lamports == 100_000
Testing Anchor Programs
To test an Anchor program with Bankrun, you’ll need to:
- Build your program with
anchor build
- Load the program binary in your test
- Create and process instructions
import asyncio
from pathlib import Path
from solders.bankrun import start
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.instruction import Instruction, AccountMeta
from solders.transaction import Transaction
async def test_anchor_program():
# Load program binary
program_id = Pubkey.from_string("YourProgramID111111111111111111111111111")
program_path = Path("target/deploy/my_program.so")
with open(program_path, "rb") as f:
program_data = f.read()
# Start test environment with program
context = await start(
[(program_id, Account(data=program_data, executable=True, owner=BPF_LOADER_UPGRADEABLE_ID))],
[]
)
# Create test accounts
user = Keypair()
my_account = Keypair()
# Airdrop SOL to user
await context.set_account(
user.pubkey(),
Account(lamports=1_000_000_000, owner=SYSTEM_PROGRAM_ID)
)
# Create initialize instruction
instruction = Instruction(
program_id=program_id,
accounts=[
AccountMeta(my_account.pubkey(), is_signer=True, is_writable=True),
AccountMeta(user.pubkey(), is_signer=True, is_writable=False),
AccountMeta(SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False),
],
data=bytes([0]), # Instruction discriminator
)
# Process transaction
tx = Transaction.new_with_payer([instruction], user.pubkey())
await context.process_transaction(tx, [user, my_account])
# Verify account was created
account = await context.get_account(my_account.pubkey())
assert account is not None
assert account.owner == program_id
# Run the test
asyncio.run(test_anchor_program())
Time Manipulation
Bankrun allows you to manipulate the clock for testing time-dependent programs:
from solders.bankrun import start
from solders.clock import Clock
# Start test environment
context = await start([], [])
# Get current clock
clock = await context.get_sysvar(Clock)
print(f"Current timestamp: {clock.unix_timestamp}")
# Advance time by 1 hour (3600 seconds)
new_clock = Clock(
slot=clock.slot + 7200, # ~1 hour at 400ms per slot
epoch_start_timestamp=clock.epoch_start_timestamp,
epoch=clock.epoch,
leader_schedule_epoch=clock.leader_schedule_epoch,
unix_timestamp=clock.unix_timestamp + 3600,
)
await context.set_sysvar(new_clock)
# Now test time-dependent logic
# ...
Comparison with Other Testing Frameworks
| Feature | Bankrun | LiteSVM | solana-test-validator |
|---|
| Language | Python | Rust, TS, Python | All |
| Speed | Fast | Fastest | Slow |
| Setup | Minimal | Minimal | Complex |
| RPC Support | No | No | Yes |
| Validator Behavior | Minimal | Minimal | Full |
| Best For | Python tests | Unit tests | Integration tests |
Testing Best Practices
- Use fixtures - Create reusable test fixtures for common setups
- Test isolation - Each test should start with a fresh context
- Async patterns - Leverage Python’s async/await for better test structure
- Error cases - Test both success and failure scenarios
- State verification - Always verify account states after operations
Example: Full Test Suite
import pytest
import asyncio
from pathlib import Path
from solders.bankrun import start
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.system_program import ID as SYSTEM_PROGRAM_ID
from solders.instruction import Instruction, AccountMeta
from solders.transaction import Transaction
class TestMyProgram:
@pytest.fixture
async def context(self):
"""Set up test context with program loaded"""
program_id = Pubkey.from_string("YourProgramID111111111111111111111111111")
program_path = Path("target/deploy/my_program.so")
with open(program_path, "rb") as f:
program_data = f.read()
ctx = await start(
[(program_id, Account(data=program_data, executable=True))],
[]
)
return ctx, program_id
@pytest.mark.asyncio
async def test_initialize(self, context):
"""Test account initialization"""
ctx, program_id = context
user = Keypair()
# Airdrop SOL
await ctx.set_account(
user.pubkey(),
Account(lamports=1_000_000_000, owner=SYSTEM_PROGRAM_ID)
)
# Test initialization
# ...
assert True # Replace with actual assertions
@pytest.mark.asyncio
async def test_invalid_input(self, context):
"""Test error handling"""
ctx, program_id = context
# Test should fail with invalid data
with pytest.raises(Exception):
# Attempt invalid operation
pass
Limitations
- No RPC methods - Direct account access only, no RPC API
- Python only - Not available for TypeScript or Rust tests
- Minimal validator behavior - Doesn’t simulate all validator features
- Limited ecosystem - Fewer examples and community resources compared to LiteSVM
When to Use solana-test-validator Instead
Consider using solana-test-validator when you need:
- Full RPC method support
- Testing validator-specific behavior
- Integration with external tools and services
- Testing complex multi-program interactions
- Realistic production-like testing environment
Additional Resources
Next Steps
- Explore [LiteSVMtesting/litesvm) for multi-language testing
- Learn about [Mollusktesting/mollusk) for Rust unit testing
- Read the [Testing Overviewtesting/overview) for testing strategies