move-unit-testing について
Sui Move のユニットテストを書く AI エージェント向けのリファレンススキルです。ドキュメントに基づき、アサーション、失敗ケース、コンテキスト、シナリオ、後処理を扱います。
主な機能
- 01テスト関数名の冗長な接頭辞を回避
- 02診断情報を示す等価アサーション
- 03想定する abort の発生箇所を指定
- 04ダミーコンテキストとシナリオの選択
- 05現行 API によるオブジェクトの後処理
活用シーン
- 01単一操作の小規模なテスト作成
- 02複数トランザクションと共有オブジェクトのテスト
- 03モジュールをまたぐ認可エラーの検証
- 04非推奨の後処理パターンの回避
MCP 対応
利用可能な環境では、検証や資料にない詳細の確認に Sui ドキュメント MCP サーバー https://sui.mcp.kapa.ai を参照するよう案内します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mystenlabs/ skills --skill move-unit-testingインストール後、Claude Code で/move-unit-testingを入力して読み込まれたか確認します。
同梱ファイルevals/code-evals.jsonevals/evals.json
詳細
move-unit-testing
MCP ツール: 環境で利用可能な場合は、最新の回答を得るために Sui ドキュメントの MCP サーバー(
https://sui.mcp.kapa.ai)にも問い合わせてください。回答の検証や、これらのリファレンスファイルに記載されていない詳細の確認に利用してください。
テストモジュールでは test_ プレフィックスを付けない
_tests モジュール内のテスト関数には、test_ プレフィックスを付けないでください。モジュール名ですでにテストであることが示されています。文として自然に読める説明的な名前を使用してください。
// 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! ではなく assert_eq! を使用する
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! に abort コードを指定しない
テストの assert! に数値の abort コードを渡さないでください。アプリケーションのエラーコードと偶然一致し、デバッグを混乱させる可能性があります。
// 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() { /* ... */ }
モジュールをまたぐ abort には location を指定した expected_failure を使用する
テストとは別のモジュールで abort が発生する場合は、location を必ず指定してください。指定しないと、テストフレームワークは abort がテストモジュールで発生すると想定し、テストが失敗します。
// 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() { /* ... */ }
location の値はモジュール名のみです(例:app)。完全修飾パス(my_package::app)ではありません。完全修飾形式を使用すると、「Unexpected module member identifier.」というコンパイルエラーが発生します。
expected_failure テストではクリーンアップを省略する
expected_failure が付いたテストは abort するため、abort の発生箇所以降のクリーンアップコードは実行されません。予期される abort の後に .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() を使用する
テストで必要なのが TxContext のみで、複数トランザクションのシミュレーションが不要な場合は、完全な test_scenario ではなく tx_context::dummy() を使用してください。test_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);
std::unit_test::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);
std::unit_test::destroy(item);
}
test_scenario を使用する場面: 共有オブジェクト、複数トランザクションのフロー、アドレス間の転送のテスト、init 関数のテスト、エポック/時間の操作。
tx_context::dummy() を使用する場面: 純粋関数のテスト、単一操作のテスト、オブジェクト作成用の ctx だけが必要なケース。
複数トランザクションおよび認可のテストには test_scenario を使用する
複数トランザクション、異なる送信者、共有オブジェクトをシミュレーションする必要がある場合や、init 関数をテストする場合は、test_scenario を使用してください。主な API は次のとおりです。
| 関数 | 用途 |
|---|---|
test_scenario::begin(@addr) | @addr を最初の送信者としてシナリオを開始する |
scenario.next_tx(@addr) | @addr を送信者とする新しいトランザクションに進む |
scenario.take_from_sender<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() | シナリオを終了する(abort しないテストでは呼び出す必要があります) |
成功テスト — 作成と検証
#[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 自体をテストから直接呼び出せないため、init_for_testing または test_init ヘルパーを公開します。これらのヘルパーを制限するには #[test_only] を使用してください。
クリーンアップには std::unit_test::destroy を使用します
テストオブジェクトのクリーンアップには std::unit_test::destroy を使用してください。従来の sui::test_utils::destroy は非推奨です。
// WRONG — deprecated
use sui::test_utils::destroy;
// WRONG — custom cleanup functions
nft.destroy_for_testing();
// CORRECT — use std::unit_test::destroy
use std::unit_test::destroy;
destroy(nft);
destroy(app);
クイックリファレンス
| パターン | 正しい例 | よくある間違い |
|---|---|---|
| テスト関数の命名 | 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 を使う |
| オブジェクトのクリーンアップ | std::unit_test::destroy(obj) | sui::test_utils::destroy(obj)(非推奨)、obj.destroy_for_testing() |