How to write tests using Trdelnik?
Initialization
Initialize the project with
# navigate to your project root directory
trdelnik init
# it will generate `.program_client` and `trdelnik-tests`
# directories with all the necessary files
trdelnik test
# view all commands
trdelnik --help
Overview
The trdlenik init command generated a skeleton for trdelnik tests. The trdelnik-tests is a rust package that willcontain all the tests. The trdelnik-tests/Cargo.toml contains all the dependencies for the tests.
The trdelnik-tests/tests/test.rs contains a test example.
Then there is the .program_client package that contains all the generated code for the program. Using this you can easily call instructions of your Anchor program. If you add any new instruction or change the derive_id!(...) program pubkey regenerate this package using trdelnik build.
Make sure your program is using a correct program ID in the derive_id!(...) macro and inside Anchor.toml.
If not, obtain the public key of a key pair you're using and replace it in these two places.
If there are any dependencies that you need when calling instructions, add them to the .program_client/src/lib.rs at top using the use keyword.
Testing
Here is a sample test that can be run using trdelnik test command. The default behaviour of is to run tests in paralell only if they are in the same file. You can run the tests parallel across multiple files using nextest. For installation instructions, see here. Then just use the this flag while running tests trdelnik test --nextest command.
#![allow(unused)] fn main() { // <my_project>/trdelnik-tests/tests/test.rs // TODO: do not forget to add all necessary dependencies to // the generated `trdelnik-tests/Cargo.toml` use program_client::my_instruction; use trdelnik_client::*; use my_program; #[throws] #[fixture] async fn init_fixture() -> Fixture { let mut validator = Validator::default(); // @todo: here you can call your add your program // validator.add_program("name", PROGRAM_ID); let client = validator.start().await; // create a test fixture let mut fixture = Fixture { client, // make sure your program is using a correct program ID program: program_keypair(1), state: keypair(42), }; // deploy a tested program fixture.deploy().await?; // call instruction init my_instruction::initialize( &fixture.client, my_instruction::instruction::Initialize { state: fixture.state.pubkey() }, my_instruction::accounts::Initialize { initializer: fixture.client.payer().pubkey(), system_program: System::id(), }, Some(fixture.state.clone()), ).await?; fixture } #[trdelnik_test] async fn test_happy_path(#[future] init_fixture: Result<Fixture>) { let fixture = init_fixture.await?; // call the instruction my_instruction::do_something( &fixture.client, "dummy_string".to_owned(), fixture.state.pubkey(), None, ).await?; // check the test result let state = fixture.get_state().await?; assert_eq!(state.something_changed, "yes"); } }
Let's break down what is happening in the test.
Fixture
First a fixture for test is initialized. The fixture is then injected into the test using the #[future] init_fixture: Result<Fixture> parameter of the test function. This is done using the amazing rstest package.
The fixture can have other fixtures as it's function parameters. You can define some common fixture for initialization and other common tasks and then just use them across all the tests.
In the fixture a local validator is started. The validator is a local Solana cluster that can be used for testing. The validator is started using the validator.start().await command. The validator is stopped when fixture goes out of scope. This allows to run multiple tests in parallel as every validator is indepenent and can process transactions without affecting each other's state.
Test function itself
Then there is the test function itself. The test function is marked with the #[trdelnik_test] macro. This is a standard rust test that can be run using cargo test as well.
You can have multiple test functions in the same file, therefore try to keep the logic inside of the tests simple.
Instructions with custom structures
- If you want to test an instruction which has custom structure as an argument
#![allow(unused)] fn main() { pub struct MyStruct { amount: u64, } // ... pub fn my_instruction(ctx: Context<Ctx>, data: MyStruct) { /* ... */ } }
- You should add an import to the
.program_clientcrate
#![allow(unused)] fn main() { // .program_client/src/lib.rs // DO NOT EDIT - automatically generated file pub mod my_program_instruction { use trdelnik_client::*; use my_program::MyStruct; // add this import // ... } }
- This file is automatically generated but the
usestatements won't be regenerated
Skipping tests
- You can add the
#[ignore]macro to skip the test.
#![allow(unused)] fn main() { #[trdelnik_test] #[ignore] async fn test() {} }
Testing programs with associated token accounts
Trdelnikdoes not exportanchor-splandspl-associated-token-account, so you have to add it manually.
# <my-project>/trdelnik-tests/Cargo.toml
# import the correct versions manually
anchor-spl = "0.24.2"
spl-associated-token-account = "1.0.3"
#![allow(unused)] fn main() { // <my-project>/trdelnik-tests/tests/test.rs use anchor_spl::token::Token; use spl_associated_token_account; async fn init_fixture() -> Fixture { // ... let account = keypair(1); let mint = keypair(2); // constructs a token mint client .create_token_mint(&mint, mint.pubkey(), None, 0) .await?; // constructs associated token account let token_account = client .create_associated_token_account(&account, mint.pubkey()) .await?; let associated_token_program = spl_associated_token_account::id(); // derives the associated token account address for the given wallet and mint let associated_token_address = spl_associated_token_account::get_associated_token_address(&account.pubkey(), mint); Fixture { // ... token_program: Token::id(), } } }
- The
trdelnik initcommand generated a dummy test suite for you. - For more details, see the complete test implementation.