Unit Testing

After you define the create function, you can test it in Sui Move using unit tests without having to go all the way through sending Sui transactions. Since Sui manages global storage separately outside of Move, there is no direct way to retrieve objects from global storage within Move. This poses a question: after calling the create function, how do we check that the object is properly transferred?

To assist easy testing in Sui Move, we provide a comprehensive testing framework in the test_scenario module that allows us to interact with objects put into the global storage. This allows us to test the behavior of any function directly in Sui Move unit tests. A lot of this is also covered in our Move testing topic.

The test_scenario emulates a series of Sui transactions, each sent from a particular address. You can start the first transaction using the test_scenario::begin function that takes the address of the user sending this transaction as an argument, and returns an instance of the Scenario struct representing a test scenario.

An instance of the Scenario struct contains a per-address object pool emulating Sui's object storage, with helper functions provided to manipulate objects in the pool. After the first transaction completes, you can start subsequent transactions using the test_scenario::next_tx function that takes an instance of the Scenario struct representing the current scenario and an address of a (new) user as arguments.

Next, write a test for the create function. Tests that need to use test_scenario must be in a separate module, either under a tests directory, or in the same file but in a module annotated with #[test_only]. This is because test_scenario itself is a test-only module, and can be used only by test-only modules.

Start the test with a hardcoded test address, which gives you a transaction context as if you sent the transaction that starts with test_scenario::begin from this address. You can then call the create function, which creates a ColorObject and transfers it to the test address:

let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
    let ctx = test_scenario::ctx(scenario);
    color_object::create(255, 0, 255, ctx);
};

Note: There is a ";" after "}". You must include ; to sequence a series of expressions, and even the block { ... } is an expression. Refer to the Move book for a detailed explanation.

After the first transaction completes (and only after the first transaction completes), address @0x1 owns the object. First, make sure it's not owned by anyone else:

let not_owner = @0x2;
// Check that not_owner does not own the just-created ColorObject.
test_scenario::next_tx(scenario, not_owner);
{
    assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};

test_scenario::next_tx switches the transaction sender to @0x2, which is a new address different from the previous one. test_scenario::has_most_recent_for_sender checks whether an object with the given type actually exists in the global storage owned by the current sender of the transaction. In this code, we assert that we should not be able to remove such an object, because @0x2 does not own any object.

Note: The second parameter of assert! is the error code. In non-test code, you usually define a list of dedicated error code constants for each type of error that could happen in production. For unit tests, it's usually unnecessary because there are too many assertions. The stack trace upon error is sufficient to tell where the error happened. You can just put 0 for assertions in unit tests.

Finally, check that @0x1 owns the object and the object value is consistent:

test_scenario::next_tx(scenario, owner);
{
    let object = test_scenario::take_from_sender<ColorObject>(scenario);
    let (red, green, blue) = color_object::get_color(&object);
    assert!(red == 255 && green == 0 && blue == 255, 0);
    test_scenario::return_to_sender(scenario, object);
};
test_scenario::end(scenario_val);

test_scenario::take_from_sender removes the object of given type from global storage that's owned by the current transaction sender (it also implicitly checks has_most_recent_for_sender). If this line of code succeeds, it means that owner indeed owns an object of type ColorObject. Also check that the field values of the object match with what you set in creation. You must return the object back to the global storage by calling test_scenario::return_to_sender so that it's back to the global storage. This also ensures that if any mutations happened to the object during the test, the global storage is aware of the changes.

Last updated