Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mystenlabs/sui/llms.txt
Use this file to discover all available pages before exploring further.
Testing is essential for building reliable smart contracts. Sui provides powerful testing tools for Move code, including unit tests and scenario-based tests.
Testing Framework Overview
Sui supports two main testing approaches:
- Unit tests: Test individual functions in isolation
- Scenario tests: Test multi-transaction flows simulating real usage
Writing Unit Tests
Unit tests use the #[test] attribute and run in a simulated environment.
Basic Test Structure
module my_package::example {
public struct Counter has key {
id: UID,
value: u64,
}
public fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
#[test]
fun test_increment() {
let mut ctx = tx_context::dummy();
let mut counter = Counter {
id: object::new(&mut ctx),
value: 0,
};
increment(&mut counter);
assert!(counter.value == 1, 0);
increment(&mut counter);
assert!(counter.value == 2, 1);
let Counter { id, value: _ } = counter;
object::delete(id);
}
}
Test-Only Code
Use #[test_only] for code that only exists in tests:
#[test_only]
module my_package::example_tests {
use my_package::example;
#[test]
fun test_something() {
// Test code here
}
}
Expected Failures
Test that code fails correctly:
const EInsufficientFunds: u64 = 0;
public fun withdraw(amount: u64, balance: u64): u64 {
assert!(balance >= amount, EInsufficientFunds);
balance - amount
}
#[test]
#[expected_failure(abort_code = EInsufficientFunds)]
fun test_insufficient_funds() {
withdraw(100, 50); // Should abort
}
Scenario Testing
Scenario tests simulate multi-transaction flows using test_scenario.
Example from First Package
Based on the actual Sui codebase first_package example:
#[test]
fun test_sword_transactions() {
use sui::test_scenario;
let initial_owner = @0xCAFE;
let final_owner = @0xFACE;
// First transaction: create sword
let mut scenario = test_scenario::begin(initial_owner);
{
let sword = sword_create(42, 7, scenario.ctx());
transfer::public_transfer(sword, initial_owner);
};
// Second transaction: transfer sword
scenario.next_tx(initial_owner);
{
let sword = scenario.take_from_sender<Sword>();
transfer::public_transfer(sword, final_owner);
};
// Third transaction: verify final owner has sword
scenario.next_tx(final_owner);
{
let sword = scenario.take_from_sender<Sword>();
assert!(sword.magic() == 42 && sword.strength() == 7, 1);
scenario.return_to_sender(sword);
};
scenario.end();
}
Testing Module Initialization
From the first_package example:
#[test]
fun test_module_init() {
use sui::test_scenario;
let admin = @0xAD;
let initial_owner = @0xCAFE;
// Emulate module initialization
let mut scenario = test_scenario::begin(admin);
{
init(scenario.ctx());
};
// Check forge was created with correct initial state
scenario.next_tx(admin);
{
let forge = scenario.take_from_sender<Forge>();
assert!(forge.swords_created() == 0, 1);
scenario.return_to_sender(forge);
};
// Create a sword using the forge
scenario.next_tx(admin);
{
let mut forge = scenario.take_from_sender<Forge>();
let sword = forge.new_sword(42, 7, scenario.ctx());
transfer::public_transfer(sword, initial_owner);
scenario.return_to_sender(forge);
};
scenario.end();
}
Testing Shared Objects
Test shared objects using take_shared and return_shared:
#[test]
fun test_shared_object() {
use sui::test_scenario as ts;
use sui::coin;
let admin = @0xAD;
let user = @0xUSER;
let mut scenario = ts::begin(admin);
// Admin creates shared object
{
let coin = coin::mint_for_testing<SUI>(1000, scenario.ctx());
let lender = flash_lender::new(coin.into_balance(), 10, scenario.ctx());
transfer::public_share_object(lender);
};
// User interacts with shared object
scenario.next_tx(user);
{
let mut lender = scenario.take_shared<FlashLender<SUI>>();
let (loan, receipt) = lender.loan(100, scenario.ctx());
// Use the loan...
lender.repay(loan, receipt);
ts::return_shared(lender);
};
scenario.end();
}
Real-World Example: Flash Loan Tests
From examples/move/flash_lender:
#[test]
fun test_flash_loan() {
let mut ts = ts::begin(@0x0);
// Admin creates flash lender
{
ts.next_tx(ADMIN);
let coin = coin::mint_for_testing<SUI>(100, ts.ctx());
let bal = coin.into_balance();
let cap = new(bal, 1, ts.ctx());
transfer::public_transfer(cap, ADMIN);
};
// Alice requests and repays a loan
{
ts.next_tx(ALICE);
let mut lender: FlashLender<SUI> = ts.take_shared();
let (loan, receipt) = lender.loan(10, ts.ctx());
// Simulate profit to repay
let mut profit = coin::mint_for_testing<SUI>(1, ts.ctx());
profit.join(loan);
lender.repay(profit, receipt);
ts::return_shared(lender);
};
// Admin withdraws profit
{
ts.next_tx(ADMIN);
let cap = ts.take_from_sender();
let mut lender: FlashLender<SUI> = ts.take_shared();
assert!(lender.max_loan() == 101, 0);
let coin = lender.withdraw(&cap, 1, ts.ctx());
transfer::public_transfer(coin, ADMIN);
ts::return_shared(lender);
ts.return_to_sender(cap);
};
ts.end();
}
Running Tests
Run all tests
Run specific test
sui move test test_sword_creation
Verbose output
With coverage
Filter by module
sui move test --filter sword_tests
Testing Best Practices
1. Test Object Lifecycle
Always properly handle objects in tests:
#[test]
fun test_object_lifecycle() {
let mut ctx = tx_context::dummy();
// Create
let obj = MyObject {
id: object::new(&mut ctx),
value: 42,
};
// Use
assert!(obj.value == 42, 0);
// Cleanup - objects without 'drop' must be deleted or transferred
let MyObject { id, value: _ } = obj;
object::delete(id);
}
2. Test Error Conditions
#[test]
#[expected_failure(abort_code = ENotAuthorized)]
fun test_unauthorized_access() {
// Code that should fail
}
3. Use Constants for Test Addresses
#[test_only]
const ADMIN: address = @0xAD;
#[test_only]
const ALICE: address = @0xA;
#[test_only]
const BOB: address = @0xB;
4. Test State Transitions
Verify state changes across transactions:
#[test]
fun test_counter_increments() {
let mut scenario = test_scenario::begin(@0x1);
// Create counter
{
let counter = Counter { id: object::new(scenario.ctx()), value: 0 };
transfer::share_object(counter);
};
// Increment
scenario.next_tx(@0x1);
{
let mut counter = scenario.take_shared<Counter>();
assert!(counter.value == 0, 0);
increment(&mut counter);
assert!(counter.value == 1, 1);
test_scenario::return_shared(counter);
};
scenario.end();
}
5. Test Multiple Users
#[test]
fun test_multi_user_scenario() {
let mut scenario = test_scenario::begin(ADMIN);
// Admin creates resource
scenario.next_tx(ADMIN);
{ /* ... */ };
// Alice interacts
scenario.next_tx(ALICE);
{ /* ... */ };
// Bob interacts
scenario.next_tx(BOB);
{ /* ... */ };
scenario.end();
}
Common Test Patterns
Testing with Coins
use sui::coin;
use sui::sui::SUI;
#[test]
fun test_payment() {
let mut scenario = test_scenario::begin(@0x1);
let coin = coin::mint_for_testing<SUI>(1000, scenario.ctx());
assert!(coin.value() == 1000, 0);
// Use coin in tests...
test_utils::destroy(coin);
scenario.end();
}
Testing Events
use sui::event;
public struct GameEvent has copy, drop {
winner: address,
score: u64,
}
public fun finish_game(winner: address, score: u64) {
event::emit(GameEvent { winner, score });
}
// Events are emitted but not directly testable in unit tests
// Use scenario tests and check side effects instead
Debugging Tests
Use debug::print
use std::debug;
#[test]
fun test_with_debug() {
let value = 42;
debug::print(&value);
// Continue testing...
}
#[test]
fun test_detailed() {
let mut counter = 0;
assert!(counter == 0, 0);
counter = counter + 1;
assert!(counter == 1, 1);
counter = counter + 1;
assert!(counter == 2, 2);
}
Next Steps