A fluent integration testing framework for Hytale server mods.
./gradlew publishToMavenLocalThen in your project's build.gradle.kts:
repositories {
mavenLocal()
}
dependencies {
implementation("net.mono:hytestkit:0.1.0")
}Copy the src/main/java/net/mono/hytestkit package into your project.
Generic test runner that executes scenarios and reports results.
TestRunner<MyContext> runner = new TestRunner<>(logger -> new MyContext(logger));
runner.register(new MyTestScenario());
runner.runAll(System.out::println);Fluent API for building test scenarios:
Test.<MyContext>named("my-test")
.describedAs("Tests something important")
.setup(ctx -> { /* initialization */ })
.action(ctx -> { /* do something */ })
.action(ctx -> { /* do something else */ })
.verify(ctx -> someCondition)
.onFailure("Expected X but got Y")
.teardown(ctx -> { /* cleanup */ })
.build();Interface for implementing custom test scenarios:
public class MyScenario implements TestScenario<MyContext> {
@Override
public String getName() { return "my-scenario"; }
@Override
public String getDescription() { return "Tests something"; }
@Override
public TestOutcome execute(MyContext context) {
// test logic
return TestOutcome.passed();
}
}Reusable test actions:
Actions.delay(1000) // Sleep 1 second
Actions.repeat(5, action) // Execute action 5 times
Actions.repeat(5, 100, action) // Execute 5 times with 100ms delay
Actions.retry(3, action) // Retry up to 3 times on failure
Actions.retry(3, 500, action) // Retry with 500ms delay between
Actions.sequence(action1, action2) // Execute actions in order
Actions.when(predicate, action) // Conditional execution
Actions.ifElse(pred, ifTrue, ifFalse) // Branch execution
Actions.log("message") // Log message (requires WithLogger)
Actions.store("key", value) // Store value (requires WithStorage)
Actions.markStart() // Mark timing start (requires WithTiming)Reusable test predicates:
Predicates.alwaysTrue()
Predicates.alwaysFalse()
Predicates.equals(ctx -> ctx.getValue(), expected)
Predicates.notNull(ctx -> ctx.getValue())
Predicates.greaterThan(ctx -> ctx.getNumber(), 10.0)
Predicates.between(ctx -> ctx.getNumber(), 0.0, 100.0)
Predicates.stringContains(ctx -> ctx.getText(), "substring")
Predicates.eventually(predicate, 5000) // Wait up to 5s for true
Predicates.eventually(predicate, 5000, 100) // With 100ms poll interval
Predicates.stored("key", Integer.class, v -> v > 0) // Test stored valueImplement these interfaces to enable specific Actions/Predicates:
public class MyContext implements
TestContext.WithStorage,
TestContext.WithLogger,
TestContext.WithTiming {
// WithStorage
public <V> void store(String key, V value) { ... }
public <V> V get(String key, Class<V> type) { ... }
public boolean has(String key) { ... }
// WithLogger
public void log(String message) { ... }
// WithTiming
public void markStart() { ... }
public long elapsedMs() { ... }
}The hytale subpackage provides ECS-specific utilities.
Base context with entity management:
EcsTestContext ctx = new EcsTestContext(world, System.out::println);
// Spawn entity
ctx.spawnEntity(
EcsEntityBuilder.create()
.withComponent(TransformComponent.getComponentType(), transform)
.withComponent(Velocity.getComponentType(), new Velocity()),
x, y, z
).join();Fluent entity creation:
EcsEntityBuilder.create()
.withComponent(TransformComponent.getComponentType(), transform)
.withComponent(Velocity.getComponentType(), new Velocity())
.withLazyComponent(MyComponent.getComponentType(), MyComponent::new)
.configure(holder -> { /* custom setup */ })
.withAddReason(AddReason.SPAWN)
.spawn(world, x, y, z);Entity manipulation actions:
EcsActions.teleport(x, y, z)
EcsActions.move(dx, dy, dz)
EcsActions.setVelocity(vx, vy, vz)
EcsActions.addVelocity(dvx, dvy, dvz)
EcsActions.setOnGround(true)
EcsActions.setSprinting(true)
EcsActions.removeEntity()
EcsActions.waitTicks(20) // Wait 20 ticks (1 second at 20 TPS)
EcsActions.waitTicks(20, 60) // Wait 20 ticks at 60 TPS
EcsActions.modifyComponent(type, component -> { ... })
EcsActions.onEntity(ref -> { ... })Entity state predicates:
EcsPredicates.entityIsValid()
EcsPredicates.hasComponent(componentType)
EcsPredicates.isOnGround()
EcsPredicates.isInAir()
EcsPredicates.isSprinting()
EcsPredicates.atPosition(x, y, z, tolerance)
EcsPredicates.yAbove(100.0)
EcsPredicates.yBelow(50.0)
EcsPredicates.speedAbove(5.0)
EcsPredicates.horizontalSpeedAbove(4.0)
EcsPredicates.movedDistance(startX, startY, startZ, minDistance)
EcsPredicates.component(type, comp -> comp.getValue() > 0)
EcsPredicates.componentValue(type, Component::getValue, v -> v > 0)Thread-safe entity reference wrapper:
EcsRef<EntityStore> ref = ctx.getEntityRef();
// Execute on world thread (blocking)
ref.execute(r -> {
Store<EntityStore> store = r.getStore();
// modify entity
});
// Query on world thread (blocking, returns value)
Double speed = ref.query(r -> {
Velocity v = r.getStore().getComponent(r, Velocity.getComponentType());
return Math.sqrt(v.getX()*v.getX() + v.getZ()*v.getZ());
});
// Modify component
ref.modifyComponent(Velocity.getComponentType(), vel -> vel.set(0, 0, 0));MIT