Test Scenario

For complicated transaction testing, Sui features the test_scenario module. This module provides functions to emulate transactions, define senders, and check the results of transactions.

/// This module contains a dummy store implementation where anyone can purchase
/// the same book for any amount of SUI greater than zero. The store owner can
/// collect the proceeds using the `StoreOwnerCap` capability.
///
/// In the tests section, we use the `test_scenario` module to simulate a few
/// transactions and test the store functionality. The test scenario is a very
/// powerful tool which can be used to simulate multiple transactions in a single
/// test.
///
/// The reference for this module is the "Black Books" TV series.
module examples::black_books {
    use sui::sui::SUI;
    use sui::coin::{Self, Coin};
    use sui::balance::{Self, Balance};

    /// Trying to purchase the book for 0 SUI.
    const ECantBeZero: u64 = 0;

    /// A store owner capability. Allows the owner to collect proceeds.
    public struct StoreOwnerCap has key, store { id: UID }

    /// The "Black Books" store located in London.
    /// Only sells one book: "The Little Book of Calm".
    public struct BlackBooks has key {
        id: UID,
        balance: Balance<SUI>,
    }

    /// The only book sold by the Black Books store.
    public struct LittleBookOfCalm has key, store { id: UID }

    /// Share the store object and transfer the store owner capability to the sender.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(StoreOwnerCap {
            id: object::new(ctx)
        }, ctx.sender());

        transfer::share_object(BlackBooks {
            id: object::new(ctx),
            balance: balance::zero()
        })
    }

    /// Purchase the "Little Book of Calm" for any amount of SUI greater than zero.
    public fun purchase(
        store: &mut BlackBooks, coin: Coin<SUI>, ctx: &mut TxContext
    ): LittleBookOfCalm {
        assert!(coin.value() > 0, ECantBeZero);
        store.balance.join(coin.into_balance());

        // create a new book
        LittleBookOfCalm { id: object::new(ctx) }
    }

    /// Collect the proceeds from the store and return them to the sender.
    public fun collect(
        store: &mut BlackBooks, _cap: &StoreOwnerCap, ctx: &mut TxContext
    ): Coin<SUI> {
        let amount = store.balance.value();
        store.balance.split(amount).into_coin(ctx)
    }

    // === Tests ===

    #[test_only]
    // The `init` is not run in tests, and normally a test_only function is
    // provided so that the module can be initialized in tests. Having it public
    // is important for tests located in other modules.
    public fun init_for_testing(ctx: &mut TxContext) {
        init(ctx);
    }

    // using a test-only attibute because this dependency can't be used in
    // production code and `sui move build` will complain about unused imports.
    //
    // the `sui::test_scenario` module is only available in tests.
    #[test_only] use sui::test_scenario;

    #[test]
    // This test uses `test_scenario` to emulate actions performed by 3 accounts.
    // A single scenario follows this structure:
    //
    // - `begin` - starts the first tx and creates the sceanario
    // - `next_tx` ... - starts the next tx and sets the sender
    // - `end` - wraps up the scenario
    //
    // It provides functions to start transactions, get the `TxContext, pull
    // objects from account inventory and shared pool, and check transaction
    // effects.
    //
    // In this test scenario:
    // 1. Bernard opens the store;
    // 2. Manny buys the book for 10 SUI and sends it to Fran;
    // 3. Fran sends the book back and buys it herself for 5 SUI;
    // 4. Bernard collects the proceeds and transfers the store to Fran;
    fun the_book_store_drama() {
        // it's a good idea to name addresses for readability
        // Bernard is the store owner, Manny is searching for the book,
        // and Fran is the next door store owner.
        let (bernard, manny, fran) = (@0x1, @0x2, @0x3);

        // create a test scenario with sender; initiates the first transaction
        let mut scenario = test_scenario::begin(bernard);

        // === First transaction ===

        // run the module initializer
        // we use curly braces to explicitly scope the transaction;
        {
            // `test_scenario::ctx` returns the `TxContext`
            init_for_testing(scenario.ctx());
        };

        // `next_tx` is used to initiate a new transaction in the scenario and
        // set the sender to the specified address. It returns `TransactionEffects`
        // which can be used to check object changes and events.
        let prev_effects = scenario.next_tx(manny);

        // make assertions on the effects of the first transaction
        let created_ids = prev_effects.created();
        let shared_ids = prev_effects.shared();
        let sent_ids = prev_effects.transferred_to_account();
        let events_num = prev_effects.num_user_events();

        assert!(created_ids.length() == 2, 0);
        assert!(shared_ids.length() == 1, 1);
        assert!(sent_ids.size() == 1, 2);
        assert!(events_num == 0, 3);

        // === Second transaction ===

        // we will store the `book_id` in a variable so we can use it later
        let book_id = {
            // test scenario can pull shared and sender-owned objects
            // here we pull the store from the pool
            let mut store = scenario.take_shared<BlackBooks>();
            let ctx = scenario.ctx();
            let coin = coin::mint_for_testing<SUI>(10_000_000_000, ctx);

            // call the purchase function
            let book = store.purchase(coin, ctx);
            let book_id = object::id(&book);

            // send the book to Fran
            transfer::transfer(book, fran);

            // now return the store to the pool
            test_scenario::return_shared(store);

            // return the book ID so we can use it across transactions
            book_id
        };

        // === Third transaction ===

        // next transaction - Fran looks in her inventory and finds the book
        // she decides to return it to Manny and buy another one herself
        scenario.next_tx(fran);
        {
            // objects can be taken from the sender by ID (if there's multiple)
            // or if there's only one object: `take_from_sender<T>(&scenario)`
            let book = scenario.take_from_sender_by_id<LittleBookOfCalm>(book_id);
            // send the book back to Manny
            transfer::transfer(book, manny);

            // now repeat the same steps as before
            let mut store = scenario.take_shared<BlackBooks>();
            let ctx = scenario.ctx();
            let coin = coin::mint_for_testing<SUI>(5_000_000_000, ctx);

            // same as before - purchase the book
            let book = store.purchase(coin, ctx);
            transfer::transfer(book, fran);

            // don't forget to return
            test_scenario::return_shared(store);
        };

        // === Fourth transaction ===

        // last transaction - Bernard collects the proceeds and transfers the store to Fran
        test_scenario::next_tx(&mut scenario, bernard);
        {
            let mut store = scenario.take_shared<BlackBooks>();
            let cap = scenario.take_from_sender<StoreOwnerCap>();
            let ctx = scenario.ctx();
            let coin = store.collect(&cap, ctx);

            transfer::public_transfer(coin, bernard);
            transfer::transfer(cap, fran);
            test_scenario::return_shared(store);
        };

        // finally, the test scenario needs to be finalized
        scenario.end();
    }
}