Flow tests can model any multi-step gameplay scenario. This guide covers patterns for structuring custom flows, handling edge cases, and making your flows robust and maintainable.
Basic Flow Structure
Every flow test follows the same fundamental pattern:
Setup -> Action -> Wait -> Assert -> [Repeat] -> Cleanup
The cleanup step is handled automatically by IsolationStrategy.DEDICATED_WORLD.
Template
@ HytaleSuite ( value = "My Custom Flows" , isolation = IsolationStrategy . DEDICATED_WORLD )
@ Tag ( "flow" )
public class MyCustomFlows {
@ FlowTest ( timeoutTicks = 300 )
@ DisplayName ( "Descriptive name of the scenario" )
void myFlowTest ( WorldTestContext ctx ) {
// Setup
Object entity = ctx . spawnEntity ( "hytale:kweebec" , 0 , 64 , 0 );
ctx . waitTicks ( 1 );
// Action
performAction (ctx, entity);
// Wait + Assert
Object result = ctx . awaitCondition (
() -> checkResult (ctx, entity),
100 , "Expected result within 100 ticks"
);
HytaleAssert . assertNotNull (result);
}
}
Design Patterns
Linear flow (A -> B -> C)
The simplest flow: each step depends on the previous one, executed sequentially. @ FlowTest ( timeoutTicks = 200 )
void linearFlow ( WorldTestContext ctx) {
// Step A: Create
Object entity = ctx . spawnEntity ( "hytale:kweebec" , 0 , 64 , 0 );
ctx . waitTicks ( 1 );
HytaleAssert . assertTrue ( ctx . entityExists (entity));
// Step B: Modify
ctx . setPosition (entity, 100 , 64 , 100 );
ctx . waitTicks ( 1 );
double [] pos = ctx . getPosition (entity);
HytaleAssert . assertEquals ( 100.0 , pos[ 0 ], 1.0 );
// Step C: Remove
ctx . despawn (entity);
ctx . waitTicks ( 1 );
}
Branching flow (if condition -> path A, else -> path B)
Some flows need to handle different outcomes depending on server behavior. @ FlowTest ( timeoutTicks = 300 )
void branchingFlow ( WorldTestContext ctx) {
Object entity = ctx . spawnEntity ( "hytale:kweebec" , 0 , 64 , 0 );
ctx . waitTicks ( 1 );
applyDamage (ctx, entity, 50 );
ctx . waitTicks ( 3 );
Object store = ctx . getStore ();
try {
StatsAssert . assertDead (store, entity);
ctx . log ( "Entity died from 50 damage -- verifying loot path" );
verifyLootDrops (ctx);
} catch ( Exception e ) {
ctx . log ( "Entity survived 50 damage -- verifying wounded path" );
CombatAssert . assertHealthBelow (store, entity, 100f );
}
}
Test interactions between multiple entities. @ FlowTest ( timeoutTicks = 300 )
void multiEntityFlow ( WorldTestContext ctx) {
Object attacker = ctx . spawnEntity ( "hytale:kweebec" , 0 , 64 , 0 );
Object defender = ctx . spawnEntity ( "hytale:trork" , 5 , 64 , 0 );
ctx . waitTicks ( 2 );
CombatAssert . assertAlive ( ctx . getStore (), attacker);
CombatAssert . assertAlive ( ctx . getStore (), defender);
triggerCombat (ctx, attacker, defender);
ctx . waitTicks ( 20 );
// At least one should be damaged
Object store = ctx . getStore ();
boolean anyDamaged =
isHealthReduced (store, attacker) ||
isHealthReduced (store, defender);
HytaleAssert . assertTrue ( "At least one entity should be damaged" , anyDamaged);
}
Timed flow (verify something happens within N ticks)
Use awaitCondition to verify time-bounded behavior. @ FlowTest ( timeoutTicks = 400 )
void timedFlow ( WorldTestContext ctx) {
Object entity = ctx . spawnEntity ( "hytale:kweebec" , 0 , 64 , 0 );
ctx . waitTicks ( 1 );
applyPoisonEffect (ctx, entity);
// Poison should reduce health within 100 ticks
Float reducedHealth = ctx . awaitCondition (
() -> {
float health = getHealth ( ctx . getStore (), entity);
return health < getMaxHealth ( ctx . getStore (), entity) ? health : null ;
},
100 ,
"Poison should reduce health within 100 ticks"
);
HytaleAssert . assertNotNull (reducedHealth);
ctx . log ( "Health reduced to %.1f by poison." , reducedHealth);
}
Best Practices
Keep flow tests focused on one scenario. A flow that tests spawning, combat, looting, crafting, and trading in a single method is hard to debug when it fails. Split it into smaller, targeted flows.
Use descriptive logging
Call ctx.log() at each major step. When a flow fails, the logs help you identify exactly where things went wrong.
ctx . log ( "Step 1: Spawning entity..." );
ctx . log ( "Step 2: Applying damage..." );
ctx . log ( "Step 3: Waiting for death..." );
Prefer awaitCondition over fixed waitTicks
Fixed tick waits are fragile — they may be too short on slow servers or unnecessarily long on fast ones. awaitCondition adapts automatically.
// Fragile
ctx . waitTicks ( 50 );
StatsAssert . assertDead (store, entity);
// Robust
ctx . awaitCondition (
() -> isEntityDead (store, entity) ? true : null ,
100 , "Entity should die"
);
Use @Order for dependent flows
If flow tests in the same suite depend on each other (not recommended, but sometimes necessary), use @Order to enforce execution sequence.
Set appropriate timeoutTicks
Calculate the maximum expected duration of your flow and add a safety margin. At 20 TPS:
100 ticks = 5 seconds
200 ticks = 10 seconds
600 ticks = 30 seconds
Setting timeoutTicks too low will cause false failures on loaded servers. Setting it too high will make failed tests take a long time to report. Aim for 2-3x the expected duration.
Next Steps