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}