Testing
Dubhe generates a init_test.move helper that bootstraps a fully initialized DappHub for unit tests. This page covers common testing patterns.
The test bootstrap helper
Every generated project includes sources/codegen/init_test.move:
// auto-generated — do not edit
#[test_only]
module mygame::init_test;
public fun deploy_dapp_for_testing(scenario: &mut Scenario): DappHub {
let ctx = test_scenario::ctx(scenario);
let clock = clock::create_for_testing(ctx);
let mut dh = dapp_system::create_dapp_hub_for_testing(ctx);
dubhe::genesis::run(&mut dh, &clock, ctx); // initializes the Framework
mygame::genesis::run(&mut dh, &clock, ctx); // registers your DApp + runs deploy_hook
clock::destroy_for_testing(clock);
test_scenario::next_tx(scenario, ctx.sender());
dh
}It does three things:
- Creates an in-memory
DappHub(no shared object needed in tests) - Initializes the Dubhe Framework itself (
dubhe::genesis::run) - Registers your DApp and runs your
deploy_hook(mygame::genesis::run)
Basic test structure
#[test_only]
module mygame::level_test;
use sui::test_scenario;
use mygame::init_test;
use mygame::level;
use dubhe::address_system;
#[test]
public fun test_set_and_get_level() {
let player = @0xA1;
let mut scenario = test_scenario::begin(player);
// Bootstrap the full DApp — always the first call in a test
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
let player_addr = address_system::ensure_origin(ctx);
// Initial state: no record
assert!(!level::has(&dh, player_addr));
// Write
level::set(&mut dh, player_addr, 5, ctx);
// Read back
assert!(level::get(&dh, player_addr) == 5);
// Cleanup
dh.destroy();
scenario.end();
}Simulating multiple callers
Use test_scenario::next_tx to simulate transactions from different addresses:
#[test]
public fun test_two_players_are_isolated() {
let admin = @0xA0;
let alice = @0xA1;
let bob = @0xA2;
let mut scenario = test_scenario::begin(admin);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
// Alice's transaction
test_scenario::next_tx(&mut scenario, alice);
{
let ctx = test_scenario::ctx(&mut scenario);
let alice_addr = address_system::ensure_origin(ctx);
level::set(&mut dh, alice_addr, 10, ctx);
};
// Bob's transaction
test_scenario::next_tx(&mut scenario, bob);
{
let ctx = test_scenario::ctx(&mut scenario);
let bob_addr = address_system::ensure_origin(ctx);
level::set(&mut dh, bob_addr, 99, ctx);
};
// Verify isolation — Alice's data unchanged after Bob's write
test_scenario::next_tx(&mut scenario, alice);
{
let ctx = test_scenario::ctx(&mut scenario);
let alice_addr = address_system::ensure_origin(ctx);
assert!(level::get(&dh, alice_addr) == 10);
};
dh.destroy();
scenario.end();
}Testing failure cases
Use #[expected_failure] to assert a transaction must abort:
#[test]
#[expected_failure]
public fun test_get_missing_record_aborts() {
let player = @0xA1;
let mut scenario = test_scenario::begin(player);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
let player_addr = address_system::ensure_origin(ctx);
// Reading a record that doesn't exist must abort
level::get(&dh, player_addr);
dh.destroy();
scenario.end();
}
#[test]
#[expected_failure]
public fun test_non_admin_cannot_set_free_credit() {
let attacker = @0xBAD;
let mut scenario = test_scenario::begin(attacker);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
// Must abort: caller is not the framework admin
dubhe::dapp_system::set_dapp_free_credit(
&mut dh,
dubhe::dapp_system::dapp_key<mygame::dapp_key::DappKey>(),
1_000_000,
ctx
);
dh.destroy();
scenario.end();
}Testing global resources
Global resources need no resource_account parameter:
#[test]
public fun test_global_counter_increments() {
let admin = @0xA0;
let mut scenario = test_scenario::begin(admin);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
// deploy_hook initializes total_players to 0
assert!(total_players::get(&dh) == 0);
total_players::set(&mut dh, 1, ctx);
assert!(total_players::get(&dh) == 1);
dh.destroy();
scenario.end();
}Testing deploy_hook initialization
Verify that deploy_hook set the expected initial state:
#[test]
public fun test_deploy_hook_initializes_state() {
let admin = @0xA0;
let mut scenario = test_scenario::begin(admin);
// deploy_dapp_for_testing calls deploy_hook::run internally
let dh = init_test::deploy_dapp_for_testing(&mut scenario);
// Assert whatever your deploy_hook sets
assert!(total_players::get(&dh) == 0);
assert!(game_config::get_max_level(&dh) == 100);
dh.destroy();
scenario.end();
}Testing access control
Test that only the admin can call privileged functions:
use dubhe::dapp_system;
use mygame::dapp_key::DappKey;
#[test]
#[expected_failure]
public fun test_non_admin_cannot_pause() {
let admin = @0xA0;
let attacker = @0xBAD;
let mut scenario = test_scenario::begin(admin);
let mut dh = init_test::deploy_dapp_for_testing(&mut scenario);
let dapp_key_str = dapp_system::dapp_key<DappKey>();
test_scenario::next_tx(&mut scenario, attacker);
{
let ctx = test_scenario::ctx(&mut scenario);
// Must abort: attacker is not the DApp admin
dapp_system::set_pausable(&mut dh, dapp_key_str, true, ctx);
};
dh.destroy();
scenario.end();
}Complete test file example
#[test_only]
module mygame::combat_test;
use sui::test_scenario;
use mygame::init_test;
use mygame::{level, stats};
use mygame::errors::player_not_found_error;
use dubhe::address_system;
const ALICE: address = @0xA1;
const BOB: address = @0xA2;
fun setup(scenario: &mut test_scenario::Scenario): dubhe::dapp_service::DappHub {
init_test::deploy_dapp_for_testing(scenario)
}
#[test]
public fun test_level_up_increases_level() {
let mut scenario = test_scenario::begin(ALICE);
let mut dh = setup(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
let alice = address_system::ensure_origin(ctx);
level::set(&mut dh, alice, 1, ctx);
stats::set(&mut dh, alice, 10, 100, 5, ctx);
// level_up logic
let lv = level::get(&dh, alice);
level::set(&mut dh, alice, lv + 1, ctx);
assert!(level::get(&dh, alice) == 2);
dh.destroy();
scenario.end();
}
#[test]
#[expected_failure]
public fun test_attack_aborts_for_unregistered_player() {
let mut scenario = test_scenario::begin(ALICE);
let mut dh = setup(&mut scenario);
let ctx = test_scenario::ctx(&mut scenario);
let alice = address_system::ensure_origin(ctx);
// Alice has no level record — must abort
player_not_found_error(level::has(&dh, alice));
dh.destroy();
scenario.end();
}