Loading...
Loading...
Use when writing unit tests for Move smart contracts on Sui. Applies to test function naming, assertions, test attributes, context usage, and cleanup patterns. Use whenever user asks to write tests, add tests, or test a Move module.
npx skill4agent add mystenlabs/skills move-unit-testingMCP tool: When available in your environment, also query the Sui documentation MCP server () for up-to-date answers. Use it for verification and for details not covered by these reference files.https://sui.mcp.kapa.ai
test__teststest_// WRONG — redundant prefix
module my_package::my_module_tests;
#[test]
fun test_create_pool() { /* ... */ }
#[test]
fun test_swap_fails_on_zero() { /* ... */ }
// CORRECT — descriptive statement names
module my_package::my_module_tests;
#[test]
fun create_pool_with_initial_liquidity() { /* ... */ }
#[test]
fun swap_aborts_on_zero_input() { /* ... */ }assert_eq!assert!assert_eq!assert!(x == y)assert!(x == y, 0)// WRONG — no diagnostic info on failure
assert!(result == 100);
assert!(result == expected_value, 0);
// CORRECT — shows both values on failure
use std::unit_test::assert_eq;
assert_eq!(result, 100);
assert_eq!(result, expected_value);assert!// assert! is fine for boolean checks
assert!(is_valid);
assert!(vec.length() > 0);assert!assert!// WRONG — numeric code may collide with app errors
assert!(is_success, 0);
assert!(balance > 0, 1);
// CORRECT — no abort code
assert!(is_success);
assert!(balance > 0);#[test]#[expected_failure]// WRONG — separate attributes
#[test]
#[expected_failure(abort_code = EInvalidInput, location = my_app)]
fun invalid_input_aborts() { /* ... */ }
// CORRECT — merged on one line
#[test, expected_failure(abort_code = EInvalidInput, location = my_app)]
fun invalid_input_aborts() { /* ... */ }expected_failurelocationlocation// Test module: my_package::app_tests
// Abort happens in: my_package::app
const ENotAuthorized: u64 = 0; // mirror the constant value from app module
// WRONG — no location; test fails because abort comes from `app`, not `app_tests`
#[test, expected_failure(abort_code = ENotAuthorized)]
fun unauthorized_call_aborts() { /* ... */ }
// CORRECT — location points to the module where the abort originates
#[test, expected_failure(abort_code = ENotAuthorized, location = app)]
fun unauthorized_call_aborts() { /* ... */ }locationappmy_package::appexpected_failureexpected_failure.end()// WRONG — cleanup after abort is dead code
#[test, expected_failure(abort_code = my_app::EInsufficientBalance)]
fun withdraw_more_than_balance_aborts() {
let mut scenario = test_scenario::begin(@0xA);
my_app::withdraw(1000, scenario.ctx());
scenario.end(); // never reached
}
// CORRECT — let it abort naturally
#[test, expected_failure(abort_code = my_app::EInsufficientBalance)]
fun withdraw_more_than_balance_aborts() {
let mut scenario = test_scenario::begin(@0xA);
my_app::withdraw(1000, scenario.ctx());
// no cleanup needed — test aborts above
}tx_context::dummy()TxContexttx_context::dummy()test_scenariotest_scenario// WRONG — unnecessary overhead for a simple test
#[test]
fun mint_returns_correct_value() {
let mut scenario = test_scenario::begin(@0xA);
let item = app::create_item(100, scenario.ctx());
assert_eq!(item.value(), 100);
test_utils::destroy(item);
scenario.end();
}
// CORRECT — dummy context is sufficient
#[test]
fun mint_returns_correct_value() {
let ctx = &mut tx_context::dummy();
let item = app::create_item(100, ctx);
assert_eq!(item.value(), 100);
test_utils::destroy(item);
}test_scenarioinittx_context::dummy()test_scenariotest_scenarioinit| Function | Purpose |
|---|---|
| Start a scenario with |
| Advance to a new transaction with |
| Take an owned object sent to the current sender |
| Return an owned object to the current sender |
| Take a shared object by type |
| Return a shared object |
| Check if sender has an object of type |
| Finalize the scenario (must be called in non-aborting tests) |
#[test]
fun owner_can_update_item() {
let owner = @0xA;
let mut scenario = test_scenario::begin(owner);
// Tx 1: create an item (transferred to owner inside create_item)
app::create_item(b"sword".to_string(), scenario.ctx());
// Tx 2: owner takes the item and updates it
scenario.next_tx(owner);
let mut item = scenario.take_from_sender<Item>();
app::set_name(&mut item, b"great sword".to_string());
assert_eq!(app::name(&item), b"great sword".to_string());
scenario.return_to_sender(item);
scenario.end();
}#[test, expected_failure(abort_code = app::ENotOwner, location = app)]
fun non_owner_cannot_update_item() {
let owner = @0xA;
let attacker = @0xB;
let mut scenario = test_scenario::begin(owner);
// Tx 1: owner creates a shared item
app::create_shared_item(b"shield".to_string(), scenario.ctx());
// Tx 2: attacker tries to update it — should abort
scenario.next_tx(attacker);
let mut item = scenario.take_shared<Item>();
app::admin_update(&mut item, b"hacked".to_string(), scenario.ctx());
// no cleanup — test aborts above
}#[test]
fun shared_counter_increments() {
let mut scenario = test_scenario::begin(@0xA);
// Tx 1: create and share
app::create_counter(scenario.ctx());
// Tx 2: anyone can increment
scenario.next_tx(@0xB);
let mut counter = scenario.take_shared<Counter>();
app::increment(&mut counter);
assert_eq!(app::value(&counter), 1);
test_scenario::return_shared(counter);
scenario.end();
}init#[test]
fun init_creates_admin_cap() {
let mut scenario = test_scenario::begin(@0xA);
// init is called automatically for the first tx in begin()
// if the module has an init function — but in tests you call it explicitly:
app::init_for_testing(scenario.ctx());
scenario.next_tx(@0xA);
assert!(scenario.has_most_recent_for_sender<AdminCap>());
scenario.end();
}init_for_testingtest_initinit#[test_only]test_utils::destroytest_utils::destroydestroy_for_testing// WRONG — custom cleanup functions
nft.destroy_for_testing();
app.destroy_for_testing();
// CORRECT — standard destroy
use sui::test_utils::destroy;
destroy(nft);
destroy(app);| Pattern | Correct | Common Mistake |
|---|---|---|
| Test function naming | | |
| Equality assertions | | |
| Boolean assertions | | |
| Test attributes | | Separate |
| Expected failure cleanup | Let it abort, no cleanup | Calling |
| Simple test context | | Full |
| Object cleanup | | |