Skip to main content

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

有些測試需要 Clock、Random 或 DenyList 等系統物件。這些物件在網路上具有固定地址,並於創世期間建立。在測試中,它們預設不存在,因此 Sui Framework 提供 #[test_only] 函式來建立及操作它們。

時鐘 (Clock)​

Clock 提供目前的網路時間戳記。 使用 clock::create_for_testing 建立一個時鐘,並使用僅限測試的函式操作時間:

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::create_for_testing 在測試情境內建立。然而,較佳的做法是將程式碼架構為讓核心邏輯接受 RandomGenerator 參數——如此一來,你可以在單元測試中直接使用 random::new_generator_for_testing() 建立產生器,完全略過 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() {
// 非決定性的種子,適用於模糊測試。結果會因不同次
// 執行而有所不同,因此不要斷言特定結果。
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_testing 與 balance::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)​

使用 測試情境 時,你可以透過 create_system_objects 一次建立所有系統物件。這會建立並共享 Clock、Random 與 DenyList:

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