Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Packagesoroban-testkit-fixtures
Moduleslib · builder
Defaultmock_all_auths on

What TestContext gives you

FieldValue
ctx.envA fresh Env, auth mocked unless you opt out
ctx.adminA generated Address to act as deployer / owner
ctx.usersA Vec<Address> you extend with add_user()
ctx.mock_authsThe 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();
}
When to opt out of mocked auth

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:

MethodEffect
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
}
Time and height are separate

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.

A fresh env opens at zero

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:

MethodEffect
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);
}
Identities are carried, registrations are not

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.

Authorisation policy

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 }
    }
}
}
Address generation

Behind `testutils`

Address::generate is a test-only SDK API. SDK v28 requires use soroban_sdk::testutils::Address as _; plus the testutils feature.

Roadmap

Seeded state

Registering contracts and seeding their state from the builder would turn the setup above into one line. Tracked as issue #6.