Skip to main content
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:
  1. Unit tests: Test individual functions in isolation
  2. 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

sui move test

Run specific test

sui move test test_sword_creation

Verbose output

sui move test --verbose

With coverage

sui move test --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...
}

Check intermediate state

#[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

Build docs developers (and LLMs) love