Solana for Developers
Writing Program Tests in Rust

Writing Test Utilities

Build the shared helper functions that every test in this module depends on

Before writing the actual tests, you need two shared utilities that every test file will use: one for funding keypairs with SOL and one for submitting transactions and unwrapping their errors in a useful way. Without them, every test would repeat the same boilerplate.

Funding accounts

When solana-program-test starts a test validator, it creates one pre-funded payer keypair. Every other keypair you generate during the test starts with zero SOL. That means before a keypair can sign and pay for a transaction, you need to transfer SOL to it from the payer.

Add the following to tests/utils/fund_account.rs:

tests/utils/fund_account.rs
use solana_program_test::BanksClient;
use solana_sdk::{
    program_error::ProgramError,
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    transaction::Transaction,
};

pub async fn fund_account(
    banks_client: &BanksClient,
    payer: &Keypair,
    receiver_pubkey: &Pubkey,
    amount_lamports: Option<u64>,
) -> Result<(), ProgramError> {
    let amount = amount_lamports.unwrap_or(10_000_000_000); // defaults to 10 SOL

    let transfer_instruction =
        solana_sdk::system_instruction::transfer(&payer.pubkey(), receiver_pubkey, amount);

    let recent_blockhash = banks_client
        .get_latest_blockhash()
        .await
        .map_err(|_| ProgramError::InvalidAccountData)?;

    let transfer_tx = Transaction::new_signed_with_payer(
        &[transfer_instruction],
        Some(&payer.pubkey()),
        &[payer],
        recent_blockhash,
    );

    banks_client
        .process_transaction(transfer_tx)
        .await
        .map_err(|_| ProgramError::InvalidInstructionData)?;

    Ok(())
}

pub async fn fund_keypair(
    banks_client: &BanksClient,
    payer: &Keypair,
    receiver: &Keypair,
    amount_lamports: Option<u64>,
) -> Result<(), ProgramError> {
    fund_account(banks_client, payer, &receiver.pubkey(), amount_lamports).await
}

fund_account takes the receiver's public key directly, which is useful when you only have an address. fund_keypair is a convenience wrapper for when you have the full Keypair on hand, which is the common case in tests.

Submitting transactions and extracting errors

The second utility is a submit_tx helper. Its job is to wrap an instruction in a Transaction, sign it, submit it, and return a Result<(), ProgramError>.

The reason you need a wrapper instead of calling banks_client.process_transaction directly is error unwrapping. When your program returns a ProgramError::Custom(code), BanksClient buries it inside a deeply nested error type:

BanksClientError::TransactionError(
    TransactionError::InstructionError(
        _,
        InstructionError::Custom(code)
    )
)

Without unwrapping this chain, your assert_eq!(result, ProgramError::Custom(6002)) assertions would never match. The helper does that unwrapping for you so every test can work directly with ProgramError values.

Add the following to tests/utils/submit_tx.rs:

tests/utils/submit_tx.rs
use solana_program_test::BanksClient;
use solana_sdk::{
    instruction::Instruction,
    program_error::ProgramError,
    signature::{Keypair, Signer},
    transaction::Transaction,
};

pub async fn submit_tx(
    banks_client: &mut BanksClient,
    payer: &Keypair,
    ix: Instruction,
) -> Result<(), ProgramError> {
    let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();

    let mut tx = Transaction::new_with_payer(&[ix], Some(&payer.pubkey()));
    tx.sign(&[payer], recent_blockhash);

    match banks_client.process_transaction(tx).await {
        Ok(_) => Ok(()),
        Err(transaction_error) => {
            if let solana_program_test::BanksClientError::TransactionError(tx_err) =
                transaction_error
            {
                if let solana_sdk::transaction::TransactionError::InstructionError(
                    _,
                    instruction_error,
                ) = tx_err
                {
                    if let solana_sdk::instruction::InstructionError::Custom(error_code) =
                        instruction_error
                    {
                        return Err(ProgramError::Custom(error_code));
                    }
                }
            }

            Err(ProgramError::InvalidInstructionData)
        }
    }
}

Info

The fallback Err(ProgramError::InvalidInstructionData) at the bottom catches errors that aren't Custom — for example, a missing signature or a malformed transaction. In practice you will see these only when the error originates outside your program logic.

Now that your shared utilities are in place, in the next section you will write the tests for the CreatePoll instruction.

Last updated on