Skip to main content

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:
  1. Build your program with anchor build
  2. Load the program binary in your test
  3. 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

FeatureBankrunLiteSVMsolana-test-validator
LanguagePythonRust, TS, PythonAll
SpeedFastFastestSlow
SetupMinimalMinimalComplex
RPC SupportNoNoYes
Validator BehaviorMinimalMinimalFull
Best ForPython testsUnit testsIntegration tests

Testing Best Practices

  1. Use fixtures - Create reusable test fixtures for common setups
  2. Test isolation - Each test should start with a fresh context
  3. Async patterns - Leverage Python’s async/await for better test structure
  4. Error cases - Test both success and failure scenarios
  5. 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

Build docs developers (and LLMs) love