Skip to main content

Test Scenario

The test_scenario module from the Sui Framework provides a way to simulate multi-transaction scenarios in tests. It maintains a view of the global object pool and allows you to test how objects are created, transferred, and accessed across multiple transactions.

#[test_only]
use sui::test_scenario;

Starting and Ending a Scenario

A test scenario begins with test_scenario::begin which takes the sender address as an argument. The scenario must be ended with test_scenario::end to clean up resources. Failing to end a scenario will result in a compilation error.

Note: there should be only one scenario per test. Creating multiple scenarios in the same test may produce unexpected results and should be avoided.

use sui::test_scenario;

#[test]
fun test_basic_scenario() {
let alice = @0xA;

// Start a scenario with alice as the sender
let mut scenario = test_scenario::begin(alice);

// ... perform operations ...

// End the scenario - returns TransactionEffects
scenario.end();
}

Transaction Simulation

Use next_tx to advance to a new transaction with a specified sender. Objects transferred in the previous transaction become available in the next one. Each next_tx call returns TransactionEffects containing information about what happened in the previous transaction.

use sui::test_scenario;

#[test]
fun test_multi_transaction() {
let alice = @0xA;
let bob = @0xB;

let mut scenario = test_scenario::begin(alice);

// First transaction: alice creates an object
// Objects created here are not yet in anyone's inventory

// Advance to second transaction with bob as sender
// Objects from the first transaction are now available
let _effects = scenario.next_tx(bob);

// ... bob can now access objects transferred to him ...

scenario.end();
}

Important: Objects transferred during a transaction are only available after calling next_tx. You cannot access an object in the same transaction where it was transferred.

Accessing Owned Objects

Owned objects transferred to an address can be accessed using take_from_sender or take_from_address. The object then can be passed to a function, returned with return_to_sender or return_to_address, or transferred elsewhere using public_transfer (if the object has store ability).

module book::test_scenario_example;

public struct Item has key, store {
id: UID,
value: u64,
}

public fun create(value: u64, ctx: &mut TxContext): Item {
Item { id: object::new(ctx), value }
}

public fun value(item: &Item): u64 { item.value }

#[test]
fun test_take_and_return() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Transaction 1: Create and transfer an item to alice
{
let item = create(100, scenario.ctx());
transfer::public_transfer(item, alice);
};

// Transaction 2: Alice takes the item
scenario.next_tx(alice);
{
// Take the most recent Item from sender's inventory
let item = scenario.take_from_sender<Item>();
assert_eq!(item.value(), 100);

// Return the item to sender's inventory
scenario.return_to_sender(item);
};

scenario.end();
}

Taking by ID

When multiple objects of the same type exist, use take_from_sender_by_id or take_from_address_by_id to take a specific one:

#[test]
fun test_take_by_id() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Create two items
let item1 = create(100, scenario.ctx());
let item2 = create(200, scenario.ctx());
let id1 = object::id(&item1);

transfer::public_transfer(item1, alice);
transfer::public_transfer(item2, alice);

scenario.next_tx(alice);
{
// Take the specific item by ID
let item = scenario.take_from_sender_by_id<Item>(id1);
assert_eq!(item.value(), 100);
scenario.return_to_sender(item);
};

scenario.end();
}

Checking Object Availability

Before taking an object, you can check if one exists:

#[test]
fun test_has_object() {
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// No items exist yet
assert!(!scenario.has_most_recent_for_sender<Item>());

let item = create(100, scenario.ctx());
transfer::public_transfer(item, alice);

scenario.next_tx(alice);

// Now an item exists
assert!(scenario.has_most_recent_for_sender<Item>());

scenario.end();
}

Accessing Shared Objects

Shared objects are accessed using take_shared and must be returned with return_shared:

module book::shared_counter;

public struct Counter has key {
id: UID,
value: u64,
}

public fun create(ctx: &mut TxContext) {
transfer::share_object(Counter {
id: object::new(ctx),
value: 0,
})
}

public fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}

public fun value(counter: &Counter): u64 { counter.value }

#[test]
fun test_shared_object() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let bob = @0xB;

let mut scenario = test_scenario::begin(alice);

// Alice creates a shared counter
create(scenario.ctx());

// Bob increments it
scenario.next_tx(bob);
{
let mut counter = scenario.take_shared<Counter>();
counter.increment();
assert_eq!(counter.value(), 1);
test_scenario::return_shared(counter);
};

// Alice increments it again
scenario.next_tx(alice);
{
let mut counter = scenario.take_shared<Counter>();
counter.increment();
assert_eq!(counter.value(), 2);
test_scenario::return_shared(counter);
};

scenario.end();
}

The with_shared Macro

For cleaner code, use the with_shared! macro which handles take and return automatically:

#[test]
fun test_with_shared_macro() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

create(scenario.ctx());
scenario.next_tx(alice);

scenario.with_shared!<Counter>(|counter, _scenario| {
counter.increment();
assert_eq!(counter.value(), 1);
});

scenario.end();
}

Accessing Immutable Objects

Immutable (frozen) objects are accessed with take_immutable and returned with return_immutable:

module book::immutable_config;

public struct Config has key {
id: UID,
max_value: u64,
}

public fun create(max_value: u64, ctx: &mut TxContext) {
transfer::freeze_object(Config {
id: object::new(ctx),
max_value,
})
}

public fun max_value(config: &Config): u64 { config.max_value }

#[test]
fun test_immutable_object() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Create an immutable config
create(1000, scenario.ctx());

scenario.next_tx(alice);
{
// Take the immutable object
let config = scenario.take_immutable<Config>();
assert_eq!(config.max_value(), 1000);

// Return it to the global inventory
test_scenario::return_immutable(config);
};

scenario.end();
}

Accessing Transaction Context

The ctx method provides access to the TxContext for the current transaction. Use it when calling functions that require a context:

#[test]
fun test_context_access() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Access the transaction context
let ctx = scenario.ctx();

// Use it for operations that need context
let item = create(100, ctx);
transfer::public_transfer(item, alice);

// The sender matches what we passed to begin()
assert_eq!(ctx.sender(), alice);

scenario.end();
}

Reading Transaction Effects

Both next_tx and end return TransactionEffects which contains information about what happened during the transaction:

#[test]
fun test_transaction_effects() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let bob = @0xB;
let mut scenario = test_scenario::begin(alice);

// Create objects in first transaction
let item1 = create(100, scenario.ctx());
let item2 = create(200, scenario.ctx());
transfer::public_transfer(item1, alice);
transfer::public_transfer(item2, bob);

// Get effects from the first transaction
let effects = scenario.next_tx(alice);

// Check what was created
assert_eq!(effects.created().length(), 2);

// Check transfers to accounts
assert_eq!(effects.transferred_to_account().size(), 2);

// Check number of events emitted
assert_eq!(effects.num_user_events(), 0);

scenario.end();
}

Available Effect Fields

MethodReturnsDescription
created()vector<ID>Objects created in this transaction
written()vector<ID>Objects modified in this transaction
deleted()vector<ID>Objects deleted in this transaction
transferred_to_account()VecMap<ID, address>Objects transferred to addresses
transferred_to_object()VecMap<ID, ID>Objects transferred to other objects
shared()vector<ID>Objects shared in this transaction
frozen()vector<ID>Objects frozen in this transaction
num_user_events()u64Number of events emitted

System Objects

Use create_system_objects to make system objects like Clock, Random, and DenyList available in tests. For more detailed coverage of testing with system objects, see Using System Objects.

use sui::clock::Clock;

#[test]
fun test_with_clock() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Create system objects (Clock, Random, DenyList)
scenario.create_system_objects();

scenario.next_tx(alice);
{
// Now Clock is available as a shared object
let clock = scenario.take_shared<Clock>();
assert_eq!(clock.timestamp_ms(), 0);
test_scenario::return_shared(clock);
};

scenario.end();
}

Epoch and Time Manipulation

Test time-dependent logic using next_epoch and later_epoch:

#[test]
fun test_epoch_advancement() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let alice = @0xA;
let mut scenario = test_scenario::begin(alice);

// Check initial epoch
assert_eq!(scenario.ctx().epoch(), 0);

// Advance to next epoch
scenario.next_epoch(alice);
assert_eq!(scenario.ctx().epoch(), 1);

// Advance epoch and time together (1000ms = 1 second)
scenario.later_epoch(1000, alice);
assert_eq!(scenario.ctx().epoch(), 2);
assert_eq!(scenario.ctx().epoch_timestamp_ms(), 1000);

scenario.end();
}

Complete Example

Here's a complete example testing a simple token transfer flow:

module book::simple_token;

public struct Token has key, store {
id: UID,
amount: u64,
}

public fun mint(amount: u64, ctx: &mut TxContext): Token {
Token { id: object::new(ctx), amount }
}

public fun amount(token: &Token): u64 { token.amount }

#[test]
fun test_token_transfer_flow() {
use std::unit_test::assert_eq;
use sui::test_scenario;

let admin = @0xAD;
let alice = @0xA;
let bob = @0xB;

// Start scenario as admin
let mut scenario = test_scenario::begin(admin);

// Admin mints tokens for alice
{
let token = mint(1000, scenario.ctx());
transfer::public_transfer(token, alice);
};

// Alice receives and transfers to bob
scenario.next_tx(alice);
{
assert!(scenario.has_most_recent_for_sender<Token>());
let token = scenario.take_from_sender<Token>();
assert_eq!(token.amount(), 1000);
transfer::public_transfer(token, bob);
};

// Bob receives the token
scenario.next_tx(bob);
{
let token = scenario.take_from_sender<Token>();
assert_eq!(token.amount(), 1000);
scenario.return_to_sender(token);
};

// Verify final state via effects
let effects = scenario.end();
assert_eq!(effects.transferred_to_account().size(), 0); // No transfers in final tx
}

Summary

FunctionPurpose
begin(sender)Start a new scenario
end(scenario)End the scenario and get final effects
next_tx(scenario, sender)Advance to next transaction
ctx(scenario)Get mutable reference to TxContext
take_from_sender<T>Take owned object from sender
return_to_sender(obj)Return object to sender
take_shared<T>Take shared object
return_shared(obj)Return shared object
take_immutable<T>Take immutable object
return_immutable(obj)Return immutable object
create_system_objectsCreate Clock, Random, DenyList
next_epochAdvance to next epoch
later_epoch(ms, sender)Advance epoch and time

Further Reading