Skip to main content

在測試中建立與使用系統物件 (Creating and Using System Objects in Tests)

有些測試需要 ClockRandomDenyList 等系統物件。這些物件在網路上有 固定地址,並且在創世(genesis)時建立。在測試中,它們預設不存在,因此 Sui Framework 提供了 #[test_only] 函式來建立與操作這些物件。

時鐘 (Clock)

Clock 提供目前的網路時間戳記。 使用 clock::create_for_testing 建立一個,並用 test-only 函式操作時間:

use std::unit_test::assert_eq;
use sui::clock;

#[test]
fun test_clock() {
let ctx = &mut tx_context::dummy();
let mut clock = clock::create_for_testing(ctx);

// 從 0 開始
assert_eq!(clock.timestamp_ms(), 0);

// 增加時間(以毫秒為單位)
clock.increment_for_testing(1000);
assert_eq!(clock.timestamp_ms(), 1000);

// 設定絕對時間(必須 >= 目前時間)
clock.set_for_testing(5000);
assert_eq!(clock.timestamp_ms(), 5000);

// 清理 - Clock 沒有 `drop`
clock.destroy_for_testing();
}

若要在測試情境中共享 Clock 以供使用,呼叫 share_for_testing

#[test]
fun test_shared_clock() {
let ctx = &mut tx_context::dummy();
let clock = clock::create_for_testing(ctx);
clock.share_for_testing();
}

Random 隨機性 (Random)

Random 物件提供鏈上隨機性。在測試中,完整的 Random 共享物件只能透過 random::create_for_testing測試情境中建立。然而,較好的做法是將程式碼結構設計成核心邏輯接收 RandomGenerator 參數——這樣就能在單元測試中直接用 random::new_generator_for_testing() 建立 generator,完全繞過 Random 物件。這種方式比較好處理,因為 Random 需要 entry 函式(無法回傳不可丟棄的值),使得斷言結果變得困難。

use sui::random::{Self, Random, RandomGenerator};

// 若要使用 Random,函式必須有 `entry` 修飾詞,因此無法回傳
// 值,也不容易測試。
entry fun my_entry_function(r: &Random, ctx: &mut TxContext) {
let mut gen = random::new_generator(r, ctx);
let result = inner_function(&mut gen);
result.destroy_or!(abort);
}

// 內部函式的範例,比進入點更容易測試。
public(package) fun inner_function(gen: &mut RandomGenerator): Option<u64> {
if (gen.generate_bool()) {
option::some(gen.generate_u64())
} else {
option::none()
}
}

#[test]
fun test_simple_random() {
// 非決定性種子,適合用於 fuzzing。每次執行結果都不同,
// 所以不要斷言特定結果。
let mut gen = random::new_generator_for_testing();
let _result = inner_function(&mut gen);

// 決定性(相同種子可重現)
let seed: vector<u8> = "Arbitrary seed bytes";
let mut gen = random::new_generator_from_seed_for_testing(seed);
assert!(inner_function(&mut gen).is_none());

// 不同的種子會得到不同——但仍可重現——的結果
let mut gen = random::new_generator_from_seed_for_testing("move book");
assert!(inner_function(&mut gen).is_some());
}

對於接收完整 Random 共享物件的進入點(唯一可能的方式是以參考 &Random 傳入),請使用測試情境

use sui::random::{Self, Random};
use sui::test_scenario;

#[test]
fun test_random_shared() {
let mut scenario = test_scenario::begin(@0x0);

// 建立並共享 Random
random::create_for_testing(scenario.ctx());
scenario.next_tx(@0x0);

let mut random = scenario.take_shared<Random>();

// 以 32 位元組的隨機性初始化(使用前必須執行)
random.update_randomness_state_for_testing(
0,
x"2020202020202020202020202020202020202020202020202020202020202020",
scenario.ctx(),
);

my_entry_function(&random, scenario.ctx());

test_scenario::return_shared(random);
scenario.end();
}

拒絕清單 (DenyList)

DenyList 用於受監管的代幣,以封鎖特定地址。可用 new_for_testing 建立本地實例,或用 create_for_testing 建立共享實例:

use sui::deny_list;
use sui::test_scenario;
use std::unit_test::destroy;

#[test]
fun test_deny_list() {
let mut scenario = test_scenario::begin(@0x0);

// 建立本地實例以進行簡單測試
let deny_list = deny_list::new_for_testing(scenario.ctx());
// ... 使用 deny_list
destroy(deny_list);

// 或建立共享的 DenyList
deny_list::create_for_testing(scenario.ctx());
scenario.next_tx(@0x0);
// ... take_shared 並使用

scenario.end();
}

Coin 與 Balance (Coin and Balance)

如需測試 coin,請使用 coin::mint_for_testingbalance::create_for_testing

use std::unit_test::assert_eq;
use sui::coin;
use sui::balance;
use sui::sui::SUI;

#[test]
fun test_coins() {
let ctx = &mut tx_context::dummy();

// 建立任意型別的 coin
let coin = coin::mint_for_testing<SUI>(1000, ctx);
assert_eq!(coin.value(), 1000);

// 銷毀並取回數值
let value = coin.burn_for_testing();
assert_eq!(value, 1000);

// 直接建立 balance
let balance = balance::create_for_testing<SUI>(500);
let value = balance.destroy_for_testing();
assert_eq!(value, 500);
}

一次建立所有系統物件 (Create All System Objects at Once)

使用 Test Scenario 時,你可以用 create_system_objects 一次建立所有系統物件。這會建立並共享 ClockRandomDenyList

use sui::clock::Clock;
use sui::random::Random;
use sui::deny_list::DenyList;
use sui::test_scenario;

#[test]
fun test_with_all_system_objects() {
let mut scenario = test_scenario::begin(@0xA);

// 以共享物件的形式建立 Clock、Random 與 DenyList
// (會推進交易,所以它們會立即可用)
scenario.create_system_objects();

// 依型別取得物件
let clock = scenario.take_shared<Clock>();
let random = scenario.take_shared<Random>();
let deny_list = scenario.take_shared<DenyList>();

// ... 使用這些物件

// 使用完畢後歸還
test_scenario::return_shared(clock);
test_scenario::return_shared(random);
test_scenario::return_shared(deny_list);

scenario.end();
}

在測試中建立的系統物件不會有在正式網路上那種固定地址。 請使用 take_shared<T>() 依型別存取它們,而不是依 ID。

若要依 ID 取得特定的共享物件,請使用 take_shared_by_id

use sui::test_scenario::{Self, most_recent_id_shared};

#[test]
fun test_take_by_id() {
let mut scenario = test_scenario::begin(@0xA);
scenario.create_system_objects();

// 取得最近一次共享的 Clock 的 ID
let clock_id = most_recent_id_shared<Clock>().destroy_some();

// 依 ID 取得
let clock = scenario.take_shared_by_id<Clock>(clock_id);
// ...
test_scenario::return_shared(clock);

scenario.end();
}

總結 (Summary)

物件建立方式僅供測試使用的功能
Clockclock::create_for_testing(ctx)increment_for_testing, set_for_testing
Randomrandom::create_for_testing(ctx)update_randomness_state_for_testing
RandomGeneratorrandom::new_generator_for_testing()new_generator_from_seed_for_testing
DenyListdeny_list::create_for_testing(ctx)new_for_testing
Coin<T>coin::mint_for_testing<T>(value, ctx)burn_for_testing
Balance<T>balance::create_for_testing<T>(value)destroy_for_testing
所有系統物件scenario.create_system_objects()建立 Clock、Random、DenyList