soroban-testkit-fixtures
A reusable test context inspired by Foundry’s setUp(): one place to build the environment, admin and user addresses every test needs.
What TestContext gives you
| Field | Value |
|---|---|
ctx.env | A fresh Env, auth mocked unless you opt out |
ctx.admin | A generated Address to act as deployer / owner |
ctx.users | A Vec<Address> you extend with add_user() |
ctx.mock_auths | The auth policy the context was built with, re-applied by reset() |
Basic usage
#![allow(unused)]
fn main() {
use soroban_testkit_fixtures::TestContext;
let ctx = TestContext::new();
// ctx.env — pre-configured Env with mock_all_auths
// ctx.admin — generated admin address
// ctx.users — empty vec, add users as needed
}
Builder pattern
Describe the context you want instead of assembling it:
#![allow(unused)]
fn main() {
use soroban_testkit_fixtures::builder::TestContextBuilder;
let ctx = TestContextBuilder::new()
.with_users(5) // Pre-generate 5 user addresses
.without_mock_auths() // Disable automatic auth mocking
.build();
}
Keep mock_all_auths for behaviour tests. Call without_mock_auths() as soon as a test asserts who had to sign — that is AuthMatcher territory.
Moving the ledger clock
Time-dependent contracts — vesting, auctions, lockups — need the ledger to
advance between two calls. TestContext exposes the four operations that cover
almost all of that without building a LedgerInfo by hand:
| Method | Effect |
|---|---|
ctx.timestamp() | The unix timestamp the ledger reports |
ctx.sequence() | The ledger sequence number the ledger reports |
ctx.set_timestamp(t) | Jump to absolute time t |
ctx.advance_time(s) | Add s seconds, return the new timestamp |
ctx.advance_ledger(n) | Add n to the sequence, return the new height |
#![allow(unused)]
fn main() {
use soroban_testkit_fixtures::TestContext;
let mut ctx = TestContext::new();
ctx.set_timestamp(1_700_000_000);
// ... assert the position is locked
let unlocked_at = ctx.advance_time(86_400 * 30); // 30 days later
assert_eq!(unlocked_at, 1_702_592_000);
// ... assert the position is claimable
}
Advance only what you assert
advance_time leaves the sequence alone and advance_ledger leaves the clock alone. A real ledger close moves both, so a test that wants the full picture calls each — and a test that only cares about TTL expiry does not silently move the clock.
Set before you advance
The SDK test env starts at timestamp 0 and sequence 0, so advance_time(3_600) lands on 3_600 rather than on wall-clock time. Pin an absolute moment with set_timestamp first when the test reads better as a date.
Both helpers saturate instead of wrapping: advancing past u64::MAX or
u32::MAX returns the maximum rather than panicking or rolling the clock back
to a date in 1970.
Resetting the chain underneath a test
Some scenarios are the same actors on a clean chain twice: a phase-one
initialize, then a phase-two that must not see phase-one’s storage. reset
gives you that without rebuilding the fixture:
| Method | Effect |
|---|---|
ctx.reset() | Fresh env — ledger, events, auths and contract storage all cleared — with the same admin and users |
ctx.reset_full() | Fresh env and new identities: a new admin and a new users list of the same length |
#![allow(unused)]
fn main() {
let mut ctx = TestContextBuilder::new().with_users(2).build();
let env = ctx.env.clone();
let (vault, client) = register_vault(&env);
client.deposit(&ctx.users[0], &1_000);
ctx.reset();
// ctx.admin and ctx.users are the same actors; the chain remembers nothing.
// `vault` named a contract in the env that is gone, so register again:
let env = ctx.env.clone();
let (vault, client) = register_vault(&env);
assert_eq!(client.balance(&vault, &ctx.users[0]), 0);
}
Re-register after resetting
An Address is a handle into the env that made it, so reset rebuilds each one from its ScAddress form and the strkey survives intact. A registered contract cannot travel that way — its code and storage live in the old env — so the id you held before the reset points at nothing. Reach for a second TestContext instead whenever the two phases want different fixtures, user counts or auth policies.
Reset keeps the policy, not by magic
TestContext remembers whether it mocks authorizations, so an unmocked context stays unmocked through a reset. An Env does not report its own policy, which means with_env has to assume the mocked default — set ctx.mock_auths = false when it is not what you configured.
reset_full exists because SDK v28 mints test addresses from a counter that
restarts at 1 in every Env. Generating straight away would hand back exactly
the addresses you replaced, so it first walks the new env’s counter past the
range the old identities occupied.
Extending fixtures
Compose TestContext into project-specific fixtures so setup is written once per repo:
#![allow(unused)]
fn main() {
struct TokenTestEnv {
ctx: TestContext,
token_id: BytesN<32>,
}
impl TokenTestEnv {
fn setup() -> Self {
let ctx = TestContextBuilder::new().with_users(3).build();
let token_id = ctx.env.register(TokenContract, ());
Self { ctx, token_id }
}
}
}
Behind `testutils`
Address::generate is a test-only SDK API. SDK v28 requires use soroban_sdk::testutils::Address as _; plus the testutils feature.
Seeded state
Registering contracts and seeding their state from the builder would turn the setup above into one line. Tracked as issue #6.