move-unit-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

move-unit-testing

move-unit-testing

MCP tool: When available in your environment, also query the Sui documentation MCP server (
https://sui.mcp.kapa.ai
) for up-to-date answers. Use it for verification and for details not covered by these reference files.
MCP工具: 如果你的环境中可用,也可以查询Sui文档MCP服务器(
https://sui.mcp.kapa.ai
)获取最新答案。用它来验证以及获取这些参考文件未涵盖的细节。

Overview

概述

AI agents consistently use outdated or suboptimal patterns when writing Move unit tests. This skill covers the correct testing conventions from the official Sui Move code quality checklist and testing documentation.
AI代理在编写Move单元测试时经常使用过时或次优的模式。本技能涵盖了官方Sui Move代码质量检查表和测试文档中的正确测试规范。

No
test_
Prefix in Test Modules

测试模块中不要使用
test_
前缀

Test functions inside
_tests
modules should NOT be prefixed with
test_
. The module name already indicates these are tests. Use descriptive names that read as statements.
move
// 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() { /* ... */ }
_tests
模块内的测试函数不应以
test_
作为前缀。模块名称已经表明这些是测试代码,应使用可读性强的陈述式名称。
move
// 错误写法 — 前缀冗余
module my_package::my_module_tests;

#[test]
fun test_create_pool() { /* ... */ }

#[test]
fun test_swap_fails_on_zero() { /* ... */ }

// 正确写法 — 描述性陈述名称
module my_package::my_module_tests;

#[test]
fun create_pool_with_initial_liquidity() { /* ... */ }

#[test]
fun swap_aborts_on_zero_input() { /* ... */ }

Use
assert_eq!
Instead of
assert!
for Comparisons

比较时使用
assert_eq!
而非
assert!

assert_eq!
displays both values on failure, making debugging much easier. Never use
assert!(x == y)
or
assert!(x == y, 0)
for equality checks.
move
// 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);
Use plain
assert!
only for boolean conditions where there's nothing to compare:
move
// assert! is fine for boolean checks
assert!(is_valid);
assert!(vec.length() > 0);
assert_eq!
在断言失败时会显示两个对比值,大幅提升调试效率。永远不要使用
assert!(x == y)
assert!(x == y, 0)
来进行相等性检查。
move
// 错误写法 — 失败时无诊断信息
assert!(result == 100);
assert!(result == expected_value, 0);

// 正确写法 — 失败时显示两个对比值
use std::unit_test::assert_eq;

assert_eq!(result, 100);
assert_eq!(result, expected_value);
仅在无需对比的布尔条件检查时使用普通的
assert!
move
// 布尔检查使用assert!没问题
assert!(is_valid);
assert!(vec.length() > 0);

No Abort Codes in Test
assert!

测试中的
assert!
不要使用中止码

Do not pass numeric abort codes to
assert!
in tests. They can accidentally match application error codes and confuse debugging.
move
// 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);
不要在测试的
assert!
中传入数字中止码,它们可能会意外匹配应用程序错误码,干扰调试。
move
// 错误写法 — 数字代码可能与应用错误冲突
assert!(is_success, 0);
assert!(balance > 0, 1);

// 正确写法 — 不使用中止码
assert!(is_success);
assert!(balance > 0);

Merge
#[test]
and
#[expected_failure]
on One Line

#[test]
#[expected_failure]
合并到同一行

move
// 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() { /* ... */ }
move
// 错误写法 — 属性分开
#[test]
#[expected_failure(abort_code = EInvalidInput, location = my_app)]
fun invalid_input_aborts() { /* ... */ }

// 正确写法 — 合并到同一行
#[test, expected_failure(abort_code = EInvalidInput, location = my_app)]
fun invalid_input_aborts() { /* ... */ }

expected_failure
with
location
for cross-module aborts

跨模块中止时为
expected_failure
添加
location

When the abort happens in a different module than the test, you must specify
location
. Without it, the test framework expects the abort to originate in the test module and the test fails.
move
// 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() { /* ... */ }
The
location
value is just the module name (e.g.,
app
), not the fully qualified path (
my_package::app
). Using the fully qualified form causes a compile error: "Unexpected module member identifier."
当中止发生在与测试不同的模块中时,你必须指定
location
。如果不指定,测试框架会预期中止来自测试模块,导致测试失败。
move
// 测试模块:my_package::app_tests
// 中止发生在:my_package::app

const ENotAuthorized: u64 = 0;  // 镜像app模块中的常量值

// 错误写法 — 无location;测试失败因为中止来自`app`而非`app_tests`
#[test, expected_failure(abort_code = ENotAuthorized)]
fun unauthorized_call_aborts() { /* ... */ }

// 正确写法 — location指向中止发生的模块
#[test, expected_failure(abort_code = ENotAuthorized, location = app)]
fun unauthorized_call_aborts() { /* ... */ }
location
的值只需是模块名称(如
app
),而非完整限定路径(
my_package::app
)。使用完整限定形式会导致编译错误:"Unexpected module member identifier."

Skip Cleanup in
expected_failure
Tests

expected_failure
测试跳过清理操作

Tests annotated with
expected_failure
will abort — any cleanup code after the abort point is dead code. Don't call
.end()
or destroy objects after the expected abort.
move
// 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
}
标注
expected_failure
的测试会中止,中止点之后的任何清理代码都是无效代码。不要在预期中止后调用
.end()
或销毁对象。
move
// 错误写法 — 中止后的清理代码无效
#[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(); // 永远不会执行到
}

// 正确写法 — 让其自然中止
#[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());
    // 无需清理 — 测试会在上方中止
}

Use
tx_context::dummy()
for Simple Tests

简单测试使用
tx_context::dummy()

If a test only needs a
TxContext
and doesn't need multi-transaction simulation, use
tx_context::dummy()
instead of a full
test_scenario
. Reserve
test_scenario
for tests that actually need to simulate multiple transactions, shared objects, or transfers between addresses.
move
// 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);
}
When to use
test_scenario
:
shared objects, multi-transaction flows, testing transfers between addresses, testing
init
functions, epoch/time manipulation.
When to use
tx_context::dummy()
:
pure function tests, single-operation tests, anything that just needs a ctx to create objects.
如果测试只需要
TxContext
且不需要多事务模拟,使用
tx_context::dummy()
而非完整的
test_scenario
。仅在确实需要模拟多事务、共享对象或地址间转账的测试中使用
test_scenario
move
// 错误写法 — 简单测试无需额外开销
#[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();
}

// 正确写法 — dummy上下文足够
#[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_scenario
共享对象、多事务流程、测试地址间转账、测试
init
函数、纪元/时间操作。
何时使用
tx_context::dummy()
纯函数测试、单操作测试、任何只需要上下文来创建对象的场景。

test_scenario
for Multi-Transaction and Authorization Tests

test_scenario
用于多事务和授权测试

Use
test_scenario
when you need to simulate multiple transactions, different senders, shared objects, or test
init
functions. The core API:
FunctionPurpose
test_scenario::begin(@addr)
Start a scenario with
@addr
as the first sender
scenario.next_tx(@addr)
Advance to a new transaction with
@addr
as sender
scenario.take_from_sender<T>()
Take an owned object sent to the current sender
scenario.return_to_sender(obj)
Return an owned object to the current sender
scenario.take_shared<T>()
Take a shared object by type
test_scenario::return_shared(obj)
Return a shared object
scenario.has_most_recent_for_sender<T>()
Check if sender has an object of type
T
scenario.end()
Finalize the scenario (must be called in non-aborting tests)
当你需要模拟多事务、不同发送者、共享对象或测试
init
函数时,使用
test_scenario
。核心API:
函数用途
test_scenario::begin(@addr)
@addr
作为第一个发送者启动场景
scenario.next_tx(@addr)
推进到新事务,以
@addr
作为发送者
scenario.take_from_sender<T>()
获取当前发送者拥有的类型为
T
的对象
scenario.return_to_sender(obj)
将拥有的对象返回给当前发送者
scenario.take_shared<T>()
按类型获取共享对象
test_scenario::return_shared(obj)
返回共享对象
scenario.has_most_recent_for_sender<T>()
检查发送者是否拥有类型为
T
的对象
scenario.end()
结束场景(非中止测试中必须调用)

Success test — create and verify

成功测试 — 创建并验证

move
#[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();
}
move
#[test]
fun owner_can_update_item() {
    let owner = @0xA;
    let mut scenario = test_scenario::begin(owner);

    // 事务1:创建物品(在create_item内部转移给owner)
    app::create_item(b"sword".to_string(), scenario.ctx());

    // 事务2:owner取出物品并更新
    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();
}

Unauthorized caller test

未授权调用测试

move
#[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
}
move
#[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);

    // 事务1:owner创建共享物品
    app::create_shared_item(b"shield".to_string(), scenario.ctx());

    // 事务2:攻击者尝试更新 — 应中止
    scenario.next_tx(attacker);
    let mut item = scenario.take_shared<Item>();
    app::admin_update(&mut item, b"hacked".to_string(), scenario.ctx());
    // 无需清理 — 测试会在上方中止
}

Shared object test

共享对象测试

move
#[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();
}
move
#[test]
fun shared_counter_increments() {
    let mut scenario = test_scenario::begin(@0xA);

    // 事务1:创建并共享
    app::create_counter(scenario.ctx());

    // 事务2:任何人都可以递增
    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();
}

Testing
init
functions

测试
init
函数

move
#[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();
}
Note: modules typically expose a
init_for_testing
or
test_init
helper since
init
itself is not directly callable in tests. Use
#[test_only]
to gate these helpers.
move
#[test]
fun init_creates_admin_cap() {
    let mut scenario = test_scenario::begin(@0xA);

    // 如果模块有init函数,begin()中的第一个事务会自动调用init — 但在测试中你需要显式调用:
    app::init_for_testing(scenario.ctx());

    scenario.next_tx(@0xA);
    assert!(scenario.has_most_recent_for_sender<AdminCap>());

    scenario.end();
}
注意:模块通常会暴露
init_for_testing
test_init
辅助函数,因为
init
本身在测试中无法直接调用。使用
#[test_only]
来限制这些辅助函数的可见性。

Use
test_utils::destroy
for Cleanup

使用
test_utils::destroy
进行清理

Use the standard
test_utils::destroy
function to clean up test objects. Do not write custom
destroy_for_testing
functions.
move
// WRONG — custom cleanup functions
nft.destroy_for_testing();
app.destroy_for_testing();

// CORRECT — standard destroy
use sui::test_utils::destroy;

destroy(nft);
destroy(app);
使用标准的
test_utils::destroy
函数清理测试对象,不要编写自定义的
destroy_for_testing
函数。
move
// 错误写法 — 自定义清理函数
nft.destroy_for_testing();
app.destroy_for_testing();

// 正确写法 — 标准销毁函数
use sui::test_utils::destroy;

destroy(nft);
destroy(app);

Quick Reference

快速参考

PatternCorrectCommon Mistake
Test function naming
create_pool_succeeds()
test_create_pool()
Equality assertions
assert_eq!(x, 100)
assert!(x == 100, 0)
Boolean assertions
assert!(is_valid)
assert!(is_valid, 0)
Test attributes
#[test, expected_failure(...)]
Separate
#[test]
and
#[expected_failure]
Expected failure cleanupLet it abort, no cleanupCalling
.end()
after abort
Simple test context
tx_context::dummy()
Full
test_scenario
for simple tests
Object cleanup
test_utils::destroy(obj)
obj.destroy_for_testing()
模式正确写法常见错误
测试函数命名
create_pool_succeeds()
test_create_pool()
相等性断言
assert_eq!(x, 100)
assert!(x == 100, 0)
布尔断言
assert!(is_valid)
assert!(is_valid, 0)
测试属性
#[test, expected_failure(...)]
分开使用
#[test]
#[expected_failure]
预期中止测试的清理让其自然中止,不做清理在中止后调用
.end()
简单测试上下文
tx_context::dummy()
简单测试使用完整
test_scenario
对象清理
test_utils::destroy(obj)
obj.destroy_for_testing()