Skip to content

Commit 4ed6a1d

Browse files
Rework test harness into JS-first categories
Address review feedback by re-categorising the test suite so behaviour is exercised through JS scripts in real game-test scenarios rather than trivial Java unit tests: - Wrapper coverage now drives raw JS values across typed boundaries (TestRuntime.as*), exercising the registered type wrappers end to end. - Replace the kjs_reads getter fixtures with an entity-behaviour test: a script gives a spawned zombie a held item and moves a cow, verified Java-side. - Add JS binding tests (incl. JsonIO/NBTIO file creation) and syntax tests. - Assert exact event fire counts, and route Java assertions through AssertJ (GameAsserts) so failures carry actual-vs-expected messages. - Trim builder fixtures to a few actually integrated into game tests. - Drop the JUnit unittest package in favour of the JS-driven tests.
1 parent 189ab72 commit 4ed6a1d

48 files changed

Lines changed: 644 additions & 1990 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ dependencies {
127127
}
128128
})
129129

130+
testImplementation("org.mockito:mockito-core:5.18.0")
130131
testImplementation("org.assertj:assertj-core:3.27.3")
131132
}
132133

@@ -157,8 +158,8 @@ val coverageRequested = gradle.startParameter.taskNames.any {
157158
it.substringAfterLast(':').startsWith("coverage")
158159
}
159160

160-
// The test source set holds both JUnit unit tests (...unittest, run by `test`) and the game-test
161-
// mod (...testmod, run by `runGametest`). Allow `test` to pass before any unit tests are present.
161+
// The test source set is driven by the game-test mod (...testmod, run by `runGametest`). JUnit
162+
// (`test`) is kept available as a last resort but currently holds no tests, so allow it to pass.
162163
tasks.named<Test>("test") {
163164
failOnNoDiscoveredTests = false
164165
if (coverageRequested) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dev.latvian.mods.kubejs.testmod;
2+
3+
import net.minecraft.gametest.framework.GameTestAssertException;
4+
import net.minecraft.gametest.framework.GameTestHelper;
5+
import net.minecraft.network.chat.Component;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
/// Bridges AssertJ into the game-test framework so failures carry AssertJ's rich message (e.g. the
10+
/// actual vs expected count). A raw [AssertionError] is an [Error] the framework never catches - it
11+
/// crashes the server - so these rethrow any assertion failure as a [GameTestAssertException], which
12+
/// the framework reports, and which `thenWaitUntil` retries against, like any native game-test assert.
13+
public final class GameAsserts {
14+
private GameAsserts() {
15+
}
16+
17+
/// Runs {@code assertions}, converting any [AssertionError] into a [GameTestAssertException].
18+
public static void assertj(GameTestHelper helper, Runnable assertions) {
19+
try {
20+
assertions.run();
21+
} catch (AssertionError error) {
22+
throw new GameTestAssertException(Component.literal(String.valueOf(error.getMessage())), (int) helper.getTick());
23+
}
24+
}
25+
26+
/// Asserts {@code id} was reported at least once.
27+
public static void assertFired(GameTestHelper helper, String id) {
28+
assertj(helper, () -> assertThat(TestRuntime.passed(id)).as("%s should have fired", id).isTrue());
29+
}
30+
31+
/// Asserts {@code id} was reported exactly {@code expected} times, naming the actual count on mismatch.
32+
public static void assertCount(GameTestHelper helper, String id, int expected) {
33+
assertj(helper, () -> assertThat(TestRuntime.count(id)).as("%s fire count", id).isEqualTo(expected));
34+
}
35+
36+
/// Surfaces any script-side assertion captured under {@code id} (see [TestRuntime#check]).
37+
public static void assertVerified(GameTestHelper helper, String id) {
38+
assertj(helper, () -> TestRuntime.verify(id));
39+
}
40+
}

src/test/java/dev/latvian/mods/kubejs/testmod/TestRuntime.java

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
package dev.latvian.mods.kubejs.testmod;
22

3+
import dev.latvian.mods.kubejs.color.KubeColor;
34
import dev.latvian.mods.kubejs.entity.KubeEntityEvent;
45
import dev.latvian.mods.kubejs.level.LevelBlock;
56
import dev.latvian.mods.kubejs.testmod.assertion.KubeEntityEventAssert;
67
import dev.latvian.mods.kubejs.testmod.assertion.LevelBlockAssert;
8+
import dev.latvian.mods.kubejs.util.Tristate;
79
import dev.latvian.mods.rhino.Undefined;
10+
import net.minecraft.core.BlockPos;
11+
import net.minecraft.nbt.CompoundTag;
12+
import net.minecraft.network.chat.MutableComponent;
13+
import net.minecraft.resources.Identifier;
14+
import net.minecraft.util.valueproviders.IntProvider;
15+
import net.minecraft.world.item.ItemStack;
16+
import net.minecraft.world.level.material.MapColor;
17+
import net.minecraft.world.phys.Vec3;
818
import org.assertj.core.api.AbstractBooleanAssert;
919
import org.assertj.core.api.AbstractDoubleAssert;
1020
import org.assertj.core.api.AbstractStringAssert;
@@ -13,9 +23,12 @@
1323
import org.assertj.core.api.ObjectAssert;
1424
import org.jspecify.annotations.Nullable;
1525

26+
import java.time.Duration;
1627
import java.util.Map;
1728
import java.util.Set;
29+
import java.util.UUID;
1830
import java.util.concurrent.ConcurrentHashMap;
31+
import java.util.regex.Pattern;
1932

2033
/// Bound into scripts as {@code TestRuntime} so a script can report a passing condition back to
2134
/// the game test that drives it, e.g. {@code TestRuntime.pass('block.break.dirt')}.
@@ -62,18 +75,34 @@ public static boolean passedStartup(String id) {
6275
return STARTUP.contains(id);
6376
}
6477

65-
/// Marks {@code id} reached and runs the script's assertions, capturing any [AssertionError] they
66-
/// throw so it survives the event dispatcher (which otherwise swallows handler exceptions).
78+
/// Marks {@code id} reached and runs the script's assertions, capturing any failure they throw so
79+
/// it survives to [#verify]. Catches [Throwable] (not just [AssertionError]) because a failure at
80+
/// script-load time would otherwise abort loading, and Rhino can surface an assertion wrapped in
81+
/// its own exception type - [#asAssertionError] unwraps it back to the underlying [AssertionError].
6782
public static void check(String id, Runnable assertions) {
6883
pass(id);
6984

7085
try {
7186
assertions.run();
72-
} catch (AssertionError error) {
73-
FAILURES.put(id, error);
87+
} catch (Throwable error) {
88+
FAILURES.put(id, asAssertionError(error));
7489
}
7590
}
7691

92+
private static AssertionError asAssertionError(Throwable error) {
93+
for (var current = error; current != null; current = current.getCause()) {
94+
if (current instanceof AssertionError assertionError) {
95+
return assertionError;
96+
}
97+
98+
if (current.getCause() == current) {
99+
break;
100+
}
101+
}
102+
103+
return new AssertionError(error.toString(), error);
104+
}
105+
77106
/// Re-throws any assertion failure captured under {@code id}, so the game test fails on its own
78107
/// thread with AssertJ's message instead of merely timing out.
79108
public static void verify(String id) {
@@ -119,4 +148,60 @@ public static <T> IterableAssert<T> assertThat(Iterable<? extends T> actual) {
119148
public static ObjectAssert<Object> assertThat(@Nullable Object actual) {
120149
return Assertions.assertThat(actual);
121150
}
151+
152+
/// Boundaries for the JS-driven wrapper tests. Each declares a wrapped Java type, so passing a raw
153+
/// JS value invokes the registered type wrapper and returns the coerced object for the script to
154+
/// assert on - exercising the JS->Java conversion end to end, not a static `wrap` call.
155+
156+
public static Vec3 asVec3(Vec3 value) {
157+
return value;
158+
}
159+
160+
public static BlockPos asBlockPos(BlockPos value) {
161+
return value;
162+
}
163+
164+
public static ItemStack asItemStack(ItemStack value) {
165+
return value;
166+
}
167+
168+
public static MutableComponent asComponent(MutableComponent value) {
169+
return value;
170+
}
171+
172+
public static CompoundTag asCompoundTag(CompoundTag value) {
173+
return value;
174+
}
175+
176+
public static KubeColor asColor(KubeColor value) {
177+
return value;
178+
}
179+
180+
public static Identifier asId(Identifier value) {
181+
return value;
182+
}
183+
184+
public static UUID asUUID(UUID value) {
185+
return value;
186+
}
187+
188+
public static Tristate asTristate(Tristate value) {
189+
return value;
190+
}
191+
192+
public static Duration asDuration(Duration value) {
193+
return value;
194+
}
195+
196+
public static Pattern asPattern(Pattern value) {
197+
return value;
198+
}
199+
200+
public static IntProvider asIntProvider(IntProvider value) {
201+
return value;
202+
}
203+
204+
public static MapColor asMapColor(MapColor value) {
205+
return value;
206+
}
122207
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package dev.latvian.mods.kubejs.testmod.binding;
2+
3+
import net.neoforged.testframework.DynamicTest;
4+
import net.neoforged.testframework.annotation.ForEachTest;
5+
import net.neoforged.testframework.annotation.TestHolder;
6+
import net.neoforged.testframework.gametest.EmptyTemplate;
7+
import net.neoforged.testframework.gametest.GameTest;
8+
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
import static dev.latvian.mods.kubejs.testmod.GameAsserts.assertFired;
13+
import static dev.latvian.mods.kubejs.testmod.GameAsserts.assertVerified;
14+
import static dev.latvian.mods.kubejs.testmod.GameAsserts.assertj;
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
/// Category 3. Verifies that script bindings are reachable and work (`binding_checks.js`), including
18+
/// that the file-writing bindings actually create files on disk.
19+
@ForEachTest(groups = "kubejs.binding")
20+
public class BindingTests {
21+
@GameTest
22+
@EmptyTemplate
23+
@TestHolder(value = "binding_reachable", description = "ID/Text bindings are reachable from JS and behave as expected")
24+
static void bindingReachable(final DynamicTest test) {
25+
test.onGameTest(helper -> {
26+
assertFired(helper, "binding.id");
27+
assertVerified(helper, "binding.id");
28+
assertFired(helper, "binding.text");
29+
assertVerified(helper, "binding.text");
30+
helper.succeed();
31+
});
32+
}
33+
34+
@GameTest
35+
@EmptyTemplate
36+
@TestHolder(value = "binding_file_io", description = "JsonIO/NBTIO bindings write files that exist on disk afterwards")
37+
static void bindingFileIo(final DynamicTest test) {
38+
test.onGameTest(helper -> {
39+
assertFired(helper, "binding.jsonio");
40+
assertVerified(helper, "binding.jsonio");
41+
assertj(helper, () -> assertThat(Files.isRegularFile(Path.of("kubejs/test_binding.json"))).as("JsonIO should have created the json file").isTrue());
42+
43+
assertFired(helper, "binding.nbtio");
44+
assertVerified(helper, "binding.nbtio");
45+
assertj(helper, () -> assertThat(Files.isRegularFile(Path.of("kubejs/test_binding.nbt"))).as("NBTIO should have created the nbt file").isTrue());
46+
47+
helper.succeed();
48+
});
49+
}
50+
}

0 commit comments

Comments
 (0)