Unit Testing & test_scenario

Run with sui move test or aptos move test.

25.1 Basic tests

Run with sui move test or aptos move test. Filter by name: sui move test reset.

#[test_only]
module counter::counter_tests;

use counter::counter;

#[test]
fun test_math() {
    assert!(1 + 1 == 2);
}

const EMyError: u64 = 7;

#[test]
#[expected_failure(abort_code = EMyError)]
fun test_expected_abort() {
    // Codes from other modules can be referenced by path,
    // e.g. abort_code = counter::ENotOwner, or as a literal with location = counter
    abort EMyError
}

#[test]
#[expected_failure(arithmetic_error, location = Self)]
fun test_overflow() {
    let x: u8 = 255;
    let _ = x + 1;
}

25.2 Test attributes

A test function (no parameters on Sui; Aptos may take signer params) #[test(alice = @0xA, bob = @0xB)]

Aptos: inject signers for given addresses Test passes only if it aborts #[expected_failure(abort_code = E)]...with a specific code (and optionally location = module)

#[expected_failure(arithmetic_error)], (vector_error, minor_status = 1), (out_of_gas) Specific VM failures

Item compiled only for tests (helpers, test modules, test constructors)

Sui: run with randomly generated arguments

25.3 Test-only helpers in production modules

On Sui, std::unit_test::destroy(x) (older: sui::test_utils::destroy) disposes of any value in tests.

#[test_only]
public fun init_for_testing(ctx: &mut TxContext) { init(ctx) }

#[test_only]
public fun destroy_for_testing(c: Counter) {
    let Counter { id, owner: _, value: _ } = c;
    id.delete();
}

25.4 Sui test_scenario: multi-transaction tests

test_scenario API begin(addr) / end() Start/finish a scenario

Finish the current tx, start a new one as addr &mut TxContext for the current tx

take_from_sender<T>() / return_to_sender(obj) Owned objects of the sender

take_from_address<T>(addr) / return_to_address(addr, obj) Owned objects of another address

take_shared<T>() / return_shared(obj) take_immutable<T>() / return_immutable(obj) Check an object was transferred Effects of next_tx

Returns TransactionEffects with created/transferred/shared IDs and emitted event counts

System objects in tests: sui::clock::create_for_testing(ctx), clock.increment_for_testing(ms), clock.destroy_for_testing(); sui::random::create_for_testing (via the system address); coin::mint_for_testing<T>(amount, ctx).

#[test_only]
module counter::scenario_tests;

use sui::test_scenario;
use counter::counter::{Self, Counter};

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

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

    // tx 1: Alice creates and shares the counter
    counter::create(scenario.ctx());

    // tx 2: Bob increments it
    scenario.next_tx(bob);
    {
        let mut c = scenario.take_shared<Counter>();
        c.increment();
        assert!(c.value() == 1);
        test_scenario::return_shared(c);
    };

    // tx 3: Alice resets it
    scenario.next_tx(alice);
    {
        let mut c = scenario.take_shared<Counter>();
        c.reset(scenario.ctx());
        assert!(c.value() == 0);
        test_scenario::return_shared(c);
    };

    scenario.end();
}

25.5 Aptos tests with signers and framework setup

Useful Aptos test helpers: timestamp::set_time_has_started_for_testing(&aptos_framework_signer), timestamp::fast_forward_seconds(n), coin test initializers via aptos_coin::initialize_for_test, randomness::initialize_for_testing.

#[test_only]
module my_addr::counter_tests {
    use std::signer;
    use my_addr::counter;

    #[test(user = @0x123)]
    fun test_create_and_increment(user: &signer) {
        let addr = signer::address_of(user);
        aptos_framework::account::create_account_for_test(addr);
        counter::create(user);
        counter::increment(user);
        assert!(counter::value(addr) == 1, 0);
    }

    #[test(user = @0x123)]
    #[expected_failure(abort_code = 1, location = my_addr::counter)]
    fun test_double_create_fails(user: &signer) {
        counter::create(user);
        counter::create(user);
    }
}

25.6 Coverage and CI

sui move test --coverage then sui move coverage summary / sui move coverage source --module m.

aptos move test --coverage then aptos move coverage summary.

Run tests, the linter, and (for critical code) the prover in CI on every commit.

🧪 Part II Exercises

Write a Library module with a Book struct (title, author, year) and getters; prove from a second module that you cannot construct Book directly.

Create a generic Stack<T: store> backed by a vector with push, pop, peek, size, and destroy_empty. Test it with a resource type.

Explain why public struct Wallet has drop { coins: vector<Coin<SUI>> } fails to compile, and fix it two different ways.

Implement Money<phantom Currency> with add and split; show that adding Money<USD> to Money<EUR> is a compile error.

Model a traffic light as an enum with a next function and a match; write tests covering every transition.

Write a Sui macro sum_by! that sums $f(x) over a vector reference, and an equivalent Aptos inline function with a lambda.

Rewrite a Diem-style module (no method syntax, vector::borrow, friend) to Sui Move 2024 idioms (public(package), method calls, index syntax, let mut).

Add #[error] constants (Sui) or std::error categories (Aptos) to the counter module, and write #[expected_failure] tests for each.

Emit an event on every counter increment; on your chain's testnet, query the events with the CLI or explorer.

Write a test_scenario test (Sui) or a multi-signer test (Aptos) where Alice creates an item, transfers it to Bob, and Bob verifies ownership.