- Unit tests and Integration tests
- Testing using Rust
- Testing using Typescript
- Anchor Manifest
- Forward in Time
- Best Testing practices
Context | Unit Tests | Integration Tests |
---|---|---|
Purpose | Validate the correctness of individual functions or small components in isolation. | Test the interaction between multiple components or the entire program within a Solana environment. |
Scope | Narrow, focused on specific functions or modules. | Broad, covers interactions between multiple components. |
Complexity | Relatively simple, testing one piece of code at a time. | More complex, as it tests how different parts of the program work together. |
Dependencies | Minimal, often mocked or isolated from the rest of the program. | High, requires a real or near-real Solana environment with actual dependencies. |
Execution Time | Fast, as it only tests small units of code. | Slower, as it involves more components and possibly network interactions. |
Test Data | Simplified, often hardcoded or mocked to suit the specific function under test. | Realistic, using data that closely mimics what the program would encounter in production. |
Maintenance | Easier to maintain since they focus on specific code segments. | More complex to maintain as changes in one part of the program can affect multiple tests. |
Use Cases | Ensuring specific logic works correctly (e.g., calculating a balance, performing math logic). | Ensuring the overall program behavior is correct (e.g., submitting mutliple transactions, interacting with other programs). |
Example | Testing if a specific instruction handler correctly processes inputs. | Testing if a series of transactions or interactions between accounts work as expected. |
Note
The rest of the materials will focus on integration testing.
Tip
For more details about the Unit Tests see Unit Tests. You can also check the rust-unit-tests example.
- Initialize
tests
folder - Specify desired
dev-dependencies
within theCargo.toml
- Include dumped SBF program from desired cluster (for example Mainnet) inside the
tests/fixtures
folder. - Write Tests
- Execute:
cargo test-sbf
Note
There are multiple options where to store dumped program, however tests/fixtures
is the most straight forward. For more information check Docs.
Important
Provided program name in the add_program(...)
function has to be the same as the dumped *.so
file (see Example)
Tip
By default, the testing session is executed in parallel, meaning all of the tests are executed in parallel, in order to achieve sequential behavior use:
cargo test-sbf -- --test-threads=1
Tip
To dump program from desired cluster use
# "-u m" stands for mainnet
#
solana program dump -u m <PROGRAM_ID> <PROGRAM_NAME>.so
- Initialize new Program Test Instance.
- Optionally, include other programs required for execution (for CPI calls).
- Create (optionally)multiple wallets.
- Aidrop funds.
- Start Context.
- Create Instructions.
- Process Transaction.
- Check the output.
Important
The example below and also rust-example adds Metaplex Metadata Program to the Program Test Environment in order to Initialize Metadata for Mint -> common behavior for creating Fungible and Non-Fungible Tokens.
use solana_program_test::*;
// other required use statements
use rust_tests::entry;
// Constants for program ID, program name, and associated programs.
const PROGRAM_ID: Pubkey = rust_tests::ID_CONST; // Define the program ID constant.
const PROGRAM_NAME: &str = "rust_tests"; // Define the program name.
const MPL_TOKEN_METADATA: &str = "mpl_token_metadata"; // Define the MPL Token Metadata program name.
mod instructions;
mod utils;
#[tokio::test]
async fn test_with_rust_1() {
// 1. Initialize a new ProgramTest instance with the program name, program ID, and entrypoint processor.
let mut program_test =
ProgramTest::new(PROGRAM_NAME, PROGRAM_ID, processor!(convert_entry!(entry)));
// 2. Add the MPL Token Metadata program to the test environment.
program_test.add_program(MPL_TOKEN_METADATA, mpl_token_metadata::ID, None);
// 3. Generate new keypairs for the signer and mint.
let signer = utils::generate_signer();
let mint = utils::generate_signer();
// 4. Airdrop some SOL to the signer's account to fund the test transactions.
utils::airdrop(&mut program_test, signer.pubkey(), 5 * LAMPORTS_PER_SOL);
// 5. Start the program test context, simulating a Solana runtime.
let mut program_test_context = program_test.start_with_context().await;
// 6. Do stuff here, for example construct Instructions
// 7. Process the Initialize instruction in the simulated environment.
let res = utils::process_instruction(
&mut program_test_context,
ix_initialize,
&signer.pubkey(),
signers,
)
.await;
// 8. Assert that the instruction was successful.
assert!(res.is_ok());
}
Example Anchor Tests with Bankrun
- Initialize the
tests
folder within your project. - Install the necessary dependencies for testing in TypeScript with Anchor, typically through
npm
oryarn
. - Create or update the TypeScript test file(s) in the
tests
directory. - Execute:
anchor test
Tip
Check the Anchor Manifest, for more details about setting up the solana-test-validator
or see the Reference.
- Generate a new keypair to act as a signer in your tests. This keypair will be used to sign transactions.
- Airdrop SOL to the signer's account to cover transaction fees and other operations during the test.
- Write individual test cases using the
it
function from Mocha (or any other testing framework). - Use the Anchor methods to send transactions, such as calling program methods, passing necessary accounts and signers, and ensuring the transactions are confirmed.
- After executing transactions, fetch the relevant on-chain data and assert that it matches the expected results using your testing framework's assertion library.
Important
The following example shows how the structure for Typescript test can look like for complete example check the anchor-example.
import * as anchor from "@coral-xyz/anchor"; // Import the Anchor library for interacting with Solana programs
import { Program } from "@coral-xyz/anchor"; // Import the Program type from the Anchor library
import { AnchorTests } from '../target/types/anchor_tests'; // Import the type definition for the AnchorTests program
// Describe block for the test suite for the "anchor-tests" program
describe("anchor-tests", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.AnchorTests as anchor.Program<AnchorTests>;
// 1. Generate a new Keypair that will be used as a signer in the tests
const signer = anchor.web3.Keypair.generate();
// 2.
before('Prepare', async () => {
// Airdrop SOL to the signer's public key to cover transaction fees and initialization costs
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(signer.publicKey, 5_000_000_000), // Airdropping 5 SOL
'confirmed' // Wait for the transaction to be confirmed
);
});
// 3.
it('Initialize', async () => {
// 4. Call the initialize method on the program with required arguments, accounts, and signers
await program.methods.initialize(...).accounts({...}).signers([...]).rpc({ commitment: "confirmed" });
// 5. Fetch the account data from the on-chain account after initialization
let accountData = await program.account.dataAccount.fetch(...);
// Assert that the fetched data matches the expected data
assert.strictEqual(accountData.someField.toString(), expected_data.toString());
assert.strictEqual(accountData.someOtherField.toString(), some_other_expected_data.toString());
});
});
A wallet and cluster that are used for all commands.
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
The test script is executed by anchor test
.
Tip
Other defined scripts can be run with anchor run <script>
. But beware, this command will not start the solana-test-validator.
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
The registry that is used in commands related to verifiable builds (e.g. when pushing a verifiable build with anchor publish
).
[registry]
url = "https://api.apr.dev"
Tip
Read more about registry here Publishing Source
Account resolution refers to the ability of clients to resolve accounts without having to manually specify them when sending transactions.
[features]
resolution = true
Adds a documentation requirement on use of UncheckedAccount
and AccountInfo
.
[features]
skip-lint = false
Adds a directory where you want the <idl>.ts
file to be copied when running anchor build
or anchor idl parse
.
Tip
This is helpful when you want to keep this file in version control, like when using it on the frontend, which will probably not have access to the target directory generated by anchor.
[workspace]
types = "app/src/idl/"
Sets the paths relative to the Anchor.toml to all programs in the local workspace, i.e. the path to the Cargo.toml manifest associated with each program that can be compiled by the anchor CLI.
Tip
For programs using the standard Anchor workflow, this can be omitted. For programs not written in Anchor but still want to publish, this should be added.
[workspace]
members = [
"programs/*",
"other_place/my_program"
]
The addresses of the programs in the workspace.
Tip
More programs = More addresses
[programs.localnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
Increases the time anchor waits for the solana-test-validator
to start up.
Tip
This is, for example, useful if you're cloning (see test.validator.clone
) with many accounts which increases the validator's startup time.
[test]
startup_wait = 10000
Deploys the program to solana-test-validator using --upgradeable-program
. This makes it possible to test that certain instructions can only be executed by the program's upgrade authority. The initial upgrade authority will be set to provider.wallet
.
If unspecified or explicitly set to false
, then the test program will be deployed with --bpf-program
, disabling upgrades to it.
[test]
upgradeable = true
These options are passed into the options with the same name in the solana-test-validator
cli (see solana-test-validator --help
) in commands like anchor test
.
[test.validator]
url = "https://api.mainnet-beta.solana.com" # This is the url of the cluster that accounts are cloned from (See `test.validator.clone`).
warp_slot = "1337" # Warp the ledger to `warp_slot` after starting the validator.
slots_per_epoch = "32" # Override the number of slots in an epoch (value must be >=32)
rpc_port = 8896 # Set JSON RPC on this port, and the next port for the RPC websocket.
limit_ledger_size = "1337" # Keep this amount of shreds in root slots.
ledger = "test-ledger" # Set ledger location.
gossip_port = 8994 # Gossip port number for the validator.
gossip_host = "127.0.0.1" # Gossip DNS name or IP address for the validator to advertise in gossip.
faucet_sol = "1337" # Give the faucet address this much SOL in genesis.
faucet_port = 8995 # Enable the faucet on this port.
dynamic_port_range = "1337-13337" # Range to use for dynamically assigned ports.
bind_address = "0.0.0.0"
Tip
It is better to set rpc_port
, gossip_port
and faucet_port
to different ports to prevent the test validator startup issues.
Tip
If for some reason the solana-test-validator does not start (you get the error stating Test validator does not look started...
), do not forget to look in test-ledger-log.txt
file usually located in .anchor/test-ledger/test-ledger-log.txt
. It contains information about possible invalid values in your [test.validator] config inside Anchor.toml
.
Override toolchain data in the workspace similar to rust-toolchain.toml
.
[toolchain]
anchor_version = "0.30.1" # `anchor-cli` version to use(requires `avm`)
solana_version = "1.18.17" # Solana version to use(applies to all Solana tools)
Makes commands like anchor test
start solana-test-validator
with a given program already loaded.
Important
This is one way how to use Cross Program Invocation with SBF program dumped for example from Mainnet.
Tip
To dump program from desired cluster use
# "-u m" stands for mainnet
#
solana program dump -u m <PROGRAM_ID> <PROGRAM_NAME>.so
[[test.genesis]]
address = "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX"
program = "dex.so" # path to the SBF
[[test.genesis]]
address = "22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD"
program = "swap.so" # path to the SBF
upgradeable = true
Use this to clone an account from the test.validator.url
cluster to your local cluster.
Important
This is another way how to use Cross Program Invocation with SBF program dumped for example from Mainnet.
[test.validator]
url = "https://api.mainnet-beta.solana.com"
[[test.validator.clone]]
address = "7NL2qWArf2BbEBBH1vTRZCsoNqFATTddH6h8GkVvrLpG"
[[test.validator.clone]]
address = "2RaN5auQwMdg5efgCaVqpETBV8sacWGR8tkK4m9kjo5r"
[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
Use this to upload an account from a .json
file.
Tip
To dump account from desired cluster use
# "-u m" stands for mainnet
#
solana account -u m <ACCOUNT_ADDRESS> --output json
[[test.validator.account]]
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
filename = "some_account.json"
[[test.validator.account]]
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
filename = "some_other_account.json"
Tip
ProgramTest provides multiple methods that can update the Clock Sysvar values.
Forward in time using the set_sysvar
method (also used in the rust-example).
// Function to forward the program test context time by a specified number of seconds.
pub async fn forward_time(program_test_context: &mut ProgramTestContext, seconds: i64) {
// Get the current clock state from the program test context.
let mut clock = program_test_context
.banks_client
.get_sysvar::<Clock>()
.await
.unwrap();
// Calculate the new timestamp after advancing time.
let new_timestamp = clock.unix_timestamp + seconds;
// Update the Clock instance with the new timestamp.
clock.unix_timestamp = new_timestamp;
// Update the sysvar in the program test context with the new Clock state.
program_test_context.set_sysvar(&clock);
}
Do not forward in time. Rather, update the deadline threshold to lower number (i.e. few seconds) and use sleep()
method for the desired amount. This is not the best approach, but it can still be useful.
Forward in time using Bankrun.
The code below shows example (also implemented in the bankrun-example)
// Fetch the Clock Sysvar
let clock = await test_env.context.banksClient.getClock()
// Get the current timestamp
const now = clock.unixTimestamp;
// Calculate desired future unixtimestamp
const in_future_7_days = now + BigInt(7 * 24 * 60 * 60);
// Initialize new Clock Instance
let new_clock = new Clock(clock.slot, clock.epochStartTimestamp, clock.epoch, clock.leaderScheduleEpoch, in_future_7_days);
// Set the new Clock Sysvar
test_env.context.setClock(new_clock);
Important
Even though Bankrun can be used along with the Anchor Framework, it is important to keep in mind that it does not work with the same Environment as the Anchor Framework. Anchor starts the solana-test-validator
, Bankrun uses ProgramTest under the hood, that means Transactions are processed in two different Environments. However, it might be beneficial to use Bankrun in some standalone test paths.