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
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 aTxContextfor creating objects.- Objects without
dropmust be consumed.std::unit_test::destroydisposes of any value in test code. (The oldersui::test_utils::destroyandtest_utils::assert_eqare deprecated in favour of thestd::unit_testversions.) assert_eq!prints both values when it fails, whichassert!(a == b)does not.
Running tests
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 optionOutput looks like:
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: 0Tests 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 asabort_code = other_module::ENotFound.arithmetic_error, location = Self: overflow, division by zero.out_of_gas, location = Selfandvector_error, location = ....- Bare
expected_failurepasses 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
#[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
- Testing, The Move Book
- Test Scenario, The Move Book
- Unit Testing, Move Reference
std::unit_testsource, MystenLabs/sui