soroban_test_helpers/
lib.rs

1//! # Soroban Test Helpers
2//!
3//! This crate provides helpful macros and utilities for testing Soroban smart contracts.
4//!
5//! ## Features
6//!
7//! - `#[test]` attribute macro: Simplifies writing tests for Soroban contracts by:
8//!   - Automatically creating a test environment
9//!   - Generating test addresses as needed
10//!   - Reducing boilerplate in test code
11//!
12//! ## Example
13//!
14//! ```rust,ignore
15//! use soroban_test_helpers::test;
16//! use soroban_sdk::{Env, Address};
17//!
18//! #[test]
19//! fn my_soroban_contract_test(env: Env, user: Address) {
20//!     // Test logic here
21//!     // `env` is automatically created with default settings
22//!     // `user` is automatically generated with env
23//! }
24//! ```
25
26use proc_macro::TokenStream;
27use quote::quote;
28use syn::{parse_macro_input, FnArg};
29
30/// A procedural macro for simplifying Soroban contract tests.
31///
32/// This macro transforms a function into a proper Soroban test by:
33///
34/// 1. Creating a test environment automatically
35/// 2. Generating address arguments automatically
36/// 3. Wrapping the test in a proper `#[test]` attribute
37///
38/// # Parameters
39///
40/// * The first parameter must be an environment type (`Env`) which will be instantiated using `Default::default()`
41/// * Any additional parameters will be auto-generated based on their type:
42///   - For `Address` types: generated using `Address::generate(&env)`
43///   - For other Soroban data types: must support a similar `generate(&env)` pattern
44///   - All generated values are properly passed to your test function
45///
46/// # Example
47///
48/// ```rust,no_run
49/// #[test]
50/// fn transfer_test(env: Env, sender: Address, receiver: Address) {
51///     // Test logic here
52///     // env will be created with Env::default()
53///     // sender and receiver will be created with Address::generate(&env)
54/// }
55/// ```
56#[proc_macro_attribute]
57pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
58    let item_fn = parse_macro_input!(input as syn::ItemFn);
59    let attrs = &item_fn.attrs;
60    let sig = &item_fn.sig;
61    let fn_name = &sig.ident;
62    let fn_return_type = &sig.output;
63    let fn_block = &item_fn.block;
64    let fn_args = &sig.inputs;
65
66    let arg_binding_and_ty = match fn_args
67        .into_iter()
68        .map(|arg| {
69            let FnArg::Typed(arg) = arg else {
70                return Err(syn::Error::new_spanned(
71                    arg,
72                    "unexpected receiver argument in test signature",
73                ));
74            };
75            let arg_binding = &arg.pat;
76            let arg_ty = &arg.ty;
77            Ok((arg_binding, arg_ty))
78        })
79        .collect::<Result<Vec<_>, _>>()
80    {
81        Ok(res) => res,
82        Err(err) => return err.to_compile_error().into(),
83    };
84
85    let arg_defs = arg_binding_and_ty.iter().map(|(arg_binding, arg_ty)| {
86        quote! {
87          #arg_binding: #arg_ty
88        }
89    });
90
91    // extracts the first Env argument and initializes with ::default()
92    let first_ty = arg_binding_and_ty
93        .first()
94        .map(|(_binding, ty)| ty)
95        .expect("at least one argument required");
96    let env_init = quote! { let env = <#first_ty>::default(); };
97
98    // extracts the following arguments (Addresses) and generates them passing the env as parameter.
99    let arg_inits = arg_binding_and_ty
100        .iter()
101        .enumerate()
102        .map(|(i, (_arg_binding, arg_ty))| {
103            if i == 0 {
104                quote! { env.clone() }
105            } else {
106                quote! { <#arg_ty>::generate(&env) }
107            }
108        });
109
110    quote! {
111        #( #attrs )*
112        #[test]
113        fn #fn_name() #fn_return_type {
114            #env_init
115            let test = | #( #arg_defs ),* | #fn_block;
116            test( #( #arg_inits ),* )
117        }
118    }
119    .into()
120}