suideveloper
All questions
Sui Move

How do I write and run unit tests for Sui Move?

November 6, 2026

Write tests as functions marked #[test] (in the same module or in a separate file under tests/), check results with assert! or std::unit_test::assert_eq!, and run sui move test from the package directory. A test passes if it finishes and fails if it aborts; #[expected_failure(abort_code = ...)] flips that for error cases. For anything involving objects across transactions, use sui::test_scenario.

A first test

Move
module book::counter;

#[test_only]
use std::unit_test::assert_eq;

public struct Counter has key { id: UID, value: u64 }

const EOverflow: u64 = 0;

public fun new(ctx: &mut TxContext): Counter {
    Counter { id: object::new(ctx), value: 0 }
}

public fun add(c: &mut Counter, n: u64) {
    assert!(c.value + n <= 100, EOverflow);
    c.value = c.value + n;
}

#[test]
fun add_works() {
    let mut ctx = tx_context::dummy();
    let mut c = new(&mut ctx);
    c.add(5);
    assert_eq!(c.value, 5);
    std::unit_test::destroy(c);
}

#[test, expected_failure(abort_code = EOverflow)]
fun add_too_much_fails() {
    let mut ctx = tx_context::dummy();
    let mut c = new(&mut ctx);
    c.add(101);
    std::unit_test::destroy(c);
}

Things to know:

  • Test functions take no parameters.
  • tx_context::dummy() gives you a TxContext for creating objects.
  • Objects without drop must be consumed. std::unit_test::destroy disposes of any value in test code. (The older sui::test_utils::destroy and test_utils::assert_eq are deprecated in favour of the std::unit_test versions.)
  • assert_eq! prints both values when it fails, which assert!(a == b) does not.

Running tests

Terminal
sui move test                  # all tests in the package
sui move test add_works        # only tests whose name matches the filter
sui move test --coverage       # record coverage
sui move coverage summary      # then view a per-module summary
sui move test --help           # every option

Output looks like:

Text
Running Move unit tests
[ PASS    ] 0x0::counter::add_too_much_fails
[ PASS    ] 0x0::counter::add_works
Test result: OK. Total tests: 2; passed: 2; failed: 0

Tests run in parallel, so result order varies. A test that exceeds the instruction limit shows as TIMEOUT.

Expected failures

expected_failure takes options:

  • abort_code = ECONST (recommended): passes only if the test aborts with that constant from the module that defines it. You can reference other modules' constants, such as abort_code = other_module::ENotFound.
  • arithmetic_error, location = Self: overflow, division by zero.
  • out_of_gas, location = Self and vector_error, location = ....
  • Bare expected_failure passes on any abort. Avoid it; it hides the wrong failure.

Clever errors declared with #[error] work as abort_code values too.

Test-only code

#[test_only] puts a function, struct, constant, use or whole module into test builds only. Use it for helpers like init_for_testing so other modules' tests can call your private init function.

Multi-transaction tests with test_scenario

Move
#[test]
fun shared_counter_flow() {
    use sui::test_scenario;

    let alice = @0xA;
    let mut scenario = test_scenario::begin(alice);
    {
        transfer::share_object(new(scenario.ctx()));
    };

    scenario.next_tx(alice);
    {
        let mut c = scenario.take_shared<Counter>();
        c.add(1);
        test_scenario::return_shared(c);
    };

    scenario.end();
}

Objects transferred or shared in one transaction become available only after next_tx. Use take_from_sender / return_to_sender for owned objects, take_shared / return_shared for shared ones, and take_immutable / return_immutable for frozen ones. create_system_objects makes Clock, Random and DenyList available; next_epoch and later_epoch move time forward.

Beyond unit tests

Unit tests don't run the real network. Once they pass, publish to testnet and call your functions, for example from the playground or with sui client call.

Sources

More on sui move