move-unit-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesemove-unit-testing
move-unit-testing
MCP 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
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.
All patterns sourced from https://move-book.com/guides/code-quality-checklist and https://move-book.com/testing/
AI代理在编写Move单元测试时经常使用过时或次优的模式。本技能涵盖了官方Sui Move代码质量检查表和测试文档中的正确测试规范。
No test_
Prefix in Test Modules
test_测试模块中不要使用test_
前缀
test_Test functions inside modules should NOT be prefixed with . The module name already indicates these are tests. Use descriptive names that read as statements.
_teststest_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() { /* ... */ }_teststest_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!
而非assert!
assert_eq!assert!assert_eq!assert!(x == y)assert!(x == y, 0)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 only for boolean conditions where there's nothing to compare:
assert!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!测试中的assert!
不要使用中止码
assert!Do not pass numeric abort codes to in tests. They can accidentally match application error codes and confuse debugging.
assert!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]将#[test]
和#[expected_failure]
合并到同一行
#[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_failurelocation跨模块中止时为expected_failure
添加location
expected_failurelocationWhen the abort happens in a different module than the test, you must specify . Without it, the test framework expects the abort to originate in the test module and the test fails.
locationmove
// 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 value is just the module name (e.g., ), not the fully qualified path (). Using the fully qualified form causes a compile error: "Unexpected module member identifier."
locationappmy_package::app当中止发生在与测试不同的模块中时,你必须指定。如果不指定,测试框架会预期中止来自测试模块,导致测试失败。
locationmove
// 测试模块: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() { /* ... */ }locationappmy_package::appSkip Cleanup in expected_failure
Tests
expected_failureexpected_failure
测试跳过清理操作
expected_failureTests annotated with will abort — any cleanup code after the abort point is dead code. Don't call or destroy objects after the expected abort.
expected_failure.end()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()简单测试使用tx_context::dummy()
tx_context::dummy()If a test only needs a and doesn't need multi-transaction simulation, use instead of a full . Reserve for tests that actually need to simulate multiple transactions, shared objects, or transfers between addresses.
TxContexttx_context::dummy()test_scenariotest_scenariomove
// 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 : shared objects, multi-transaction flows, testing transfers between addresses, testing functions, epoch/time manipulation.
test_scenarioinitWhen to use : pure function tests, single-operation tests, anything that just needs a ctx to create objects.
tx_context::dummy()如果测试只需要且不需要多事务模拟,使用而非完整的。仅在确实需要模拟多事务、共享对象或地址间转账的测试中使用。
TxContexttx_context::dummy()test_scenariotest_scenariomove
// 错误写法 — 简单测试无需额外开销
#[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_scenarioinit何时使用: 纯函数测试、单操作测试、任何只需要上下文来创建对象的场景。
tx_context::dummy()test_scenario
for Multi-Transaction and Authorization Tests
test_scenariotest_scenario
用于多事务和授权测试
test_scenarioUse when you need to simulate multiple transactions, different senders, shared objects, or test functions. The core API:
test_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) |
当你需要模拟多事务、不同发送者、共享对象或测试函数时,使用。核心API:
inittest_scenario| 函数 | 用途 |
|---|---|
| 以 |
| 推进到新事务,以 |
| 获取当前发送者拥有的类型为 |
| 将拥有的对象返回给当前发送者 |
| 按类型获取共享对象 |
| 返回共享对象 |
| 检查发送者是否拥有类型为 |
| 结束场景(非中止测试中必须调用) |
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测试init
函数
initmove
#[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 or helper since itself is not directly callable in tests. Use to gate these helpers.
init_for_testingtest_initinit#[test_only]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_testingtest_initinit#[test_only]Use test_utils::destroy
for Cleanup
test_utils::destroy使用test_utils::destroy
进行清理
test_utils::destroyUse the standard function to clean up test objects. Do not write custom functions.
test_utils::destroydestroy_for_testingmove
// 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::destroydestroy_for_testingmove
// 错误写法 — 自定义清理函数
nft.destroy_for_testing();
app.destroy_for_testing();
// 正确写法 — 标准销毁函数
use sui::test_utils::destroy;
destroy(nft);
destroy(app);Quick Reference
快速参考
| 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 | | |
| 模式 | 正确写法 | 常见错误 |
|---|---|---|
| 测试函数命名 | | |
| 相等性断言 | | |
| 布尔断言 | | |
| 测试属性 | | 分开使用 |
| 预期中止测试的清理 | 让其自然中止,不做清理 | 在中止后调用 |
| 简单测试上下文 | | 简单测试使用完整 |
| 对象清理 | | |