Testing

Dubhe generates an init_test.move helper that provides factory functions for creating isolated storage objects in unit tests. This page covers common testing patterns.


The test bootstrap helpers

Every generated project includes sources/codegen/init_test.move with three helpers:

// auto-generated — do not edit
module mygame::init_test;
 
/// Create a DappHub for testing without sharing it.
public fun create_dapp_hub_for_testing(ctx: &mut TxContext): dubhe::dapp_service::DappHub {
    dubhe::dapp_system::create_dapp_hub_for_testing(ctx)
}
 
/// Create a DappStorage for this DApp without sharing it.
/// Use this to test functions that access global resources or run guards.
public fun create_dapp_storage_for_testing(ctx: &mut TxContext): dubhe::dapp_service::DappStorage {
    dubhe::dapp_system::create_dapp_storage_for_testing<mygame::dapp_key::DappKey>(ctx)
}
 
/// Create a UserStorage for `owner` without sharing it.
/// Use this to test functions that read or write per-user resources.
public fun create_user_storage_for_testing(
    owner: address,
    ctx: &mut TxContext,
): dubhe::dapp_service::UserStorage {
    dubhe::dapp_system::create_user_storage_for_testing<mygame::dapp_key::DappKey>(owner, ctx)
}

These create in-memory storage objects with no gas cost — no shared objects or transactions are needed in tests.


Basic test structure

#[test_only]
module mygame::level_test;
 
use mygame::init_test;
use mygame::level;
use dubhe::dapp_service::UserStorage;
 
#[test]
public fun test_set_and_get_level() {
    let player = @0xA1;
    let ctx = &mut sui::tx_context::dummy();
 
    // Create isolated UserStorage for the player
    let mut user_storage = init_test::create_user_storage_for_testing(player, ctx);
 
    // Initial state: no record
    assert!(!level::has(&user_storage));
 
    // Write
    level::set(&mut user_storage, 5, ctx);
 
    // Read back
    assert!(level::get(&user_storage) == 5);
 
    // Cleanup
    dubhe::dapp_service::destroy_user_storage(user_storage);
}

Simulating multiple callers

Each caller gets their own UserStorage — storage isolation is guaranteed by the object model:

#[test]
public fun test_two_players_are_isolated() {
    let alice = @0xA1;
    let bob   = @0xA2;
    let ctx   = &mut sui::tx_context::dummy();
 
    // Alice and Bob each have their own UserStorage
    let mut alice_storage = init_test::create_user_storage_for_testing(alice, ctx);
    let mut bob_storage   = init_test::create_user_storage_for_testing(bob, ctx);
 
    level::set(&mut alice_storage, 10, ctx);
    level::set(&mut bob_storage, 99, ctx);
 
    // Verify isolation
    assert!(level::get(&alice_storage) == 10);
    assert!(level::get(&bob_storage) == 99);
 
    dubhe::dapp_service::destroy_user_storage(alice_storage);
    dubhe::dapp_service::destroy_user_storage(bob_storage);
}

Testing global resources

Global resources use DappStorage:

#[test]
public fun test_global_counter_increments() {
    let ctx = &mut sui::tx_context::dummy();
    let mut dapp_storage = init_test::create_dapp_storage_for_testing(ctx);
 
    // Initialize global counter
    total_players::set(&mut dapp_storage, 0, ctx);
    assert!(total_players::get(&dapp_storage) == 0);
 
    total_players::set(&mut dapp_storage, 1, ctx);
    assert!(total_players::get(&dapp_storage) == 1);
 
    dubhe::dapp_service::destroy_dapp_storage(dapp_storage);
}

Testing failure cases

Use #[expected_failure] to assert a transaction must abort:

#[test]
#[expected_failure]
public fun test_get_missing_record_aborts() {
    let ctx = &mut sui::tx_context::dummy();
    let user_storage = init_test::create_user_storage_for_testing(@0xA1, ctx);
 
    // Reading a record that doesn't exist must abort
    level::get(&user_storage);
 
    dubhe::dapp_service::destroy_user_storage(user_storage);
}

To assert a specific abort code, use #[expected_failure(abort_code = ...)]:

#[test]
#[expected_failure(abort_code = dubhe::error::ENoPermission)]
public fun test_non_admin_cannot_pause() {
    let attacker = @0xBAD;
    let ctx = &mut sui::tx_context::dummy();
    let mut dapp_storage = init_test::create_dapp_storage_for_testing(ctx);
 
    // Must abort: ctx.sender() (@0x0 in dummy) is not the DApp admin
    dapp_system::set_paused<DappKey>(&mut dapp_storage, true, ctx);
 
    dubhe::dapp_service::destroy_dapp_storage(dapp_storage);
}

Testing access control guards

ensure_latest_version checks that the DappStorage.version matches migrate::on_chain_version(). create_dapp_storage_for_testing initialises version to 1 and a freshly generated migrate.move also returns 1, so both match and the guard passes by default.

To test that a stale version is rejected, simulate an upgraded contract by setting ON_CHAIN_VERSION = 2 in migrate.move while the test storage still has version 1:

use dubhe::dapp_system;
use mygame::dapp_key::DappKey;
use mygame::migrate;
 
#[test]
public fun test_current_version_passes() {
    let ctx = &mut sui::tx_context::dummy();
    let dapp_storage = init_test::create_dapp_storage_for_testing(ctx);
 
    // Both storage version (1) and on_chain_version() (1) match — no abort
    dapp_system::ensure_latest_version<DappKey>(&dapp_storage, migrate::on_chain_version());
 
    dubhe::dapp_service::destroy_dapp_storage(dapp_storage);
}

Testing with both storage types

When testing a system function that uses both dapp_storage (guards, global resources) and user_storage (user data), create both:

#[test]
public fun test_attack_monster() {
    let player = @0xA1;
    let ctx = &mut sui::tx_context::dummy();
 
    let dapp_storage     = init_test::create_dapp_storage_for_testing(ctx);
    let mut user_storage = init_test::create_user_storage_for_testing(player, ctx);
 
    // Set up player state
    level::set(&mut user_storage, 5, ctx);
    stats::set(&mut user_storage, 100, 1000, 50, ctx); // attack, hp, defense
 
    // Call the system function under test
    // (in real test you'd call the entry fun directly; here shown as inline logic)
    error::player_not_found(level::has(&user_storage));
    let lv = level::get(&user_storage);
    level::set(&mut user_storage, lv + 1, ctx);
 
    assert!(level::get(&user_storage) == 6);
 
    dubhe::dapp_service::destroy_dapp_storage(dapp_storage);
    dubhe::dapp_service::destroy_user_storage(user_storage);
}

Complete test file example

#[test_only]
module mygame::combat_test;
 
use mygame::init_test;
use mygame::{level, stats};
use mygame::error;
 
const ALICE: address = @0xA1;
const BOB:   address = @0xA2;
 
#[test]
public fun test_level_up_increases_level() {
    let ctx = &mut sui::tx_context::dummy();
    let mut alice_storage = init_test::create_user_storage_for_testing(ALICE, ctx);
 
    level::set(&mut alice_storage, 1, ctx);
    stats::set(&mut alice_storage, 10, 100, 5, ctx);
 
    let lv = level::get(&alice_storage);
    level::set(&mut alice_storage, lv + 1, ctx);
 
    assert!(level::get(&alice_storage) == 2);
 
    dubhe::dapp_service::destroy_user_storage(alice_storage);
}
 
#[test]
#[expected_failure]
public fun test_attack_aborts_for_unregistered_player() {
    let ctx = &mut sui::tx_context::dummy();
    let alice_storage = init_test::create_user_storage_for_testing(ALICE, ctx);
 
    // Alice has no level record — must abort
    error::player_not_found(level::has(&alice_storage));
 
    dubhe::dapp_service::destroy_user_storage(alice_storage);
}

Running tests with the Dubhe CLI

From the repository root that contains dubhe.config.ts:

dubhe test

Run only tests whose fully qualified name contains a substring (same rules as the positional filter in sui move test):

dubhe test mygame::level_test::test_set_and_get_level
dubhe test --test test_set_and_get_level

List all tests without executing:

dubhe test --list

For gas limits and other flags, see the CLI test command.