Testing Patterns
Common patterns for testing Soroban contracts with Testkit: a shared setup(), multi-contract interactions, and deterministic ledger time.
AudienceContract authors
Cratesfixtures · assert
Read time4 minutes
Unit test structure
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use soroban_testkit_fixtures::builder::TestContextBuilder;
fn setup() -> TestContext {
TestContextBuilder::new()
.with_users(3)
.build()
}
#[test]
fn test_happy_path() {
let ctx = setup();
// ...
}
#[test]
#[should_panic(expected = "InsufficientBalance")]
fn test_insufficient_balance() {
let ctx = setup();
// ...
}
}
}
Cross-contract testing
Register multiple contracts and test interactions:
#![allow(unused)]
fn main() {
#[test]
fn test_contract_interaction() {
let ctx = TestContextBuilder::new().with_users(2).build();
let token_id = ctx.env.register(TokenContract, ());
let vault_id = ctx.env.register(VaultContract, (&token_id,));
let vault_client = VaultClient::new(&ctx.env, &vault_id);
vault_client.deposit(&ctx.users[0], &1000);
// Assert both contracts behaved correctly
}
}
Time-dependent tests
Manipulate ledger state for time-sensitive logic:
#![allow(unused)]
fn main() {
#[test]
fn test_vesting_unlock() {
let ctx = TestContextBuilder::new().with_users(1).build();
ctx.env.ledger().set(LedgerInfo {
timestamp: 1_700_000_000,
..Default::default()
});
// ... assert locked
ctx.env.ledger().set(LedgerInfo {
timestamp: 1_800_000_000,
..Default::default()
});
// ... assert unlocked
}
}
Reset between cases
Each TestContext owns its own Env, so ledger writes and storage never leak between tests. Share a setup() helper instead of a mutable global fixture.
Where to go next
Budget-aware testing
Pin these patterns down with CPU and memory regression assertions.
Property testing
Replace hand-picked inputs with Soroban-aware generators.
soroban-testkit-assert
The full matcher API for events and authorizations.