Skillshub minecraft-testing
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/Jahrome907/minecraft-codex-skills/minecraft-testing" ~/.claude/skills/comeonoliver-skillshub-minecraft-testing && rm -rf "$T"
manifest:
skills/Jahrome907/minecraft-codex-skills/minecraft-testing/SKILL.mdsource content
Minecraft Testing Skill
Testing Strategies Overview
| Approach | Best For | Requires Game? |
|---|---|---|
| JUnit 5 (pure unit tests) | Logic, data structures, NBT serialization | No |
| MockBukkit | Bukkit/Paper plugin events, commands, inventory | No (mocked server) |
| NeoForge GameTests | In-game block/entity/world interaction | Yes (test environment) |
| Fabric GameTests | In-game block/entity/world interaction | Yes (test environment) |
| Integration server | Full plugin/mod lifecycle | Yes (dedicated test server) |
Routing Boundaries
: the task is designing or implementing automated tests (unit, mock, gametest, CI test jobs) for Minecraft projects.Use when
: the task is implementing gameplay features rather than testing them (Do not use when
,minecraft-modding
,minecraft-plugin-dev
).minecraft-datapack
: the task is release automation or publishing pipelines (Do not use when
).minecraft-ci-release
Unit Testing (JUnit 5 — No Minecraft)
build.gradle.kts
additions
build.gradle.ktsdependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") } }
Example pure unit test
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CooldownManagerTest { @Test void playerOnCooldown_returnsFalse_afterExpiry() { var manager = new CooldownManager(500L); // 500ms cooldown manager.startCooldown("steve"); assertTrue(manager.isOnCooldown("steve")); // fast-forward time by sleeping or injecting a Clock assertFalse(manager.isOnCooldown("notExisting")); } @Test void cooldown_throwsIllegalArgument_onNegativeDuration() { assertThrows(IllegalArgumentException.class, () -> new CooldownManager(-1L)); } }
MockBukkit (Paper/Bukkit Plugin Tests)
MockBukkit provides a mock Bukkit server for unit-testing plugin logic without running a real Minecraft server.
build.gradle.kts
build.gradle.ktsrepositories { maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.mockbukkit.org/artifactory/mockbukkit/") } dependencies { compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") testImplementation("com.github.seeseemelk:MockBukkit-v1.21:3.127.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { useJUnitPlatform() }
Setup / teardown pattern
import be.seeseemelk.mockbukkit.MockBukkit; import be.seeseemelk.mockbukkit.ServerMock; import be.seeseemelk.mockbukkit.entity.PlayerMock; import org.junit.jupiter.api.*; class MyPluginTest { private static ServerMock server; private static MyPlugin plugin; @BeforeAll static void setUp() { // Start mock Bukkit server and load your plugin server = MockBukkit.mock(); plugin = MockBukkit.load(MyPlugin.class); } @AfterAll static void tearDown() { MockBukkit.unmock(); } @BeforeEach void beforeEach() { // Create a fresh mock player per test if needed } }
Testing events
@Test void playerJoin_getsWelcomeMessage() { PlayerMock player = server.addPlayer("Steve"); player.simulateJoin(); // fires PlayerJoinEvent // Assert the player received the expected message component player.assertSaid("Welcome, Steve!"); // Or for Adventure components: assertTrue(player.nextMessage().contains("Welcome")); } @Test void onBlockBreak_cancelledForNonOp() { PlayerMock player = server.addPlayer(); player.setOp(false); Block block = player.getWorld().getBlockAt(0, 64, 0); block.setType(Material.STONE); BlockBreakEvent event = new BlockBreakEvent(block, player); server.getPluginManager().callEvent(event); assertTrue(event.isCancelled(), "Non-op should not be able to break blocks"); }
Testing commands
@Test void mypluginInfo_returnsVersion() { PlayerMock player = server.addPlayer("Admin"); player.setOp(true); boolean result = server.dispatchCommand(player, "myplugin info"); assertTrue(result); player.assertSaid("Version: " + plugin.getDescription().getVersion()); } @Test void mypluginReload_requiresOp() { PlayerMock player = server.addPlayer("NonOp"); player.setOp(false); server.dispatchCommand(player, "myplugin reload"); player.assertSaid("No permission."); }
Testing inventory / items
@Test void giveKitCommand_givesPlayerItems() { PlayerMock player = server.addPlayer(); server.dispatchCommand(player, "kit starter"); // Check inventory assertTrue(player.getInventory().contains(Material.STONE_SWORD)); assertTrue(player.getInventory().contains(Material.BREAD, 16)); }
Testing scheduler tasks
@Test void repeatingTask_firesAfterDelay() { PlayerMock player = server.addPlayer(); // Execute 40 ticks worth of scheduled tasks server.getScheduler().performTicks(40L); // Assert expected side effect happened assertEquals(2, plugin.getTaskCount()); }
Testing PDC
@Test void pdcKillCount_incrementsOnKill() { PlayerMock player = server.addPlayer(); NamespacedKey key = new NamespacedKey(plugin, "kills"); // Simulate kill event EntityDeathEvent deathEvent = new EntityDeathEvent( server.addMockEntity(EntityType.ZOMBIE), new ArrayList<>(), 0 ); deathEvent.getEntity().setKiller(player); server.getPluginManager().callEvent(deathEvent); int kills = player.getPersistentDataContainer() .getOrDefault(key, PersistentDataType.INTEGER, 0); assertEquals(1, kills); }
NeoForge GameTests
GameTests run inside a Minecraft world. They place a structure (the test environment), then run assertions using
GameTestHelper.
Registration
// In your mod main class: @Mod(MyMod.MOD_ID) public class MyMod { public MyMod(IEventBus modEventBus) { modEventBus.register(MyGameTests.class); } }
Test class
import net.minecraft.gametest.framework.*; import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; @GameTestHolder(MyMod.MOD_ID) // registers test namespace @PrefixGameTestTemplate(false) // don't prefix template names public class MyGameTests { // Default template: 3x3x3 air structure called "mymod:empty" @GameTest(template = "mymod:empty") public static void testBlockInteraction(GameTestHelper helper) { // Place a block helper.setBlock(1, 1, 1, net.minecraft.world.level.block.Blocks.FURNACE); // Run after 1 tick helper.runAfterDelay(1, () -> { // Assert block state helper.assertBlock(new net.minecraft.core.BlockPos(1, 1, 1), b -> b.is(net.minecraft.world.level.block.Blocks.FURNACE), "Expected furnace"); helper.succeed(); }); } @GameTest(template = "mymod:empty", timeoutTicks = 200) public static void testEntitySpawn(GameTestHelper helper) { // Spawn entity var entity = helper.spawnWithNoFreeWill( net.minecraft.world.entity.EntityType.ZOMBIE, new net.minecraft.core.BlockPos(2, 2, 2) ); helper.runAfterDelay(5, () -> { helper.assertEntityPresent( net.minecraft.world.entity.EntityType.ZOMBIE, new net.minecraft.core.BlockPos(2, 2, 2), 1.0 ); helper.succeed(); }); } }
Structure templates (.nbt
files)
.nbtPlace empty structure files at:
src/main/resources/data/mymod/structures/empty.nbt
Generate them in-game using
/test create mymod:empty 3 3 3 (NeoForge test command).
Commit the .nbt files to version control.
Running GameTests
# Start the test server and run all tests ./gradlew runGameTestServer # In-game (dev environment): # /test runall # /test run mymod:test_block_interaction
Fabric GameTests
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; import net.minecraft.core.BlockPos; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.world.level.block.Blocks; public class MyFabricGameTests implements FabricGameTest { @GameTest(template = EMPTY_STRUCTURE) public void testCustomBlock(GameTestHelper helper) { helper.setBlock(1, 1, 1, Blocks.GOLD_BLOCK.defaultBlockState()); helper.runAfterDelay(2, () -> { helper.assertBlock( new BlockPos(1, 1, 1), b -> b.is(Blocks.GOLD_BLOCK), "Gold block should be placed" ); helper.succeed(); }); } }
Register in fabric.mod.json
fabric.mod.json{ "entrypoints": { "fabric-gametest": [ "com.example.mymod.fabric.MyFabricGameTests" ] } }
GameTestHelper
Assertions Reference
GameTestHelper// Block assertions helper.assertBlock(pos, predicate, "message"); helper.assertBlockState(pos, state -> state.is(Blocks.STONE), "Expected stone"); helper.assertBlockPresent(Blocks.GOLD_BLOCK, pos); helper.assertBlockNotPresent(Blocks.TNT, pos); // Entity assertions helper.assertEntityPresent(EntityType.ZOMBIE, pos, radius); helper.assertEntityNotPresent(EntityType.ZOMBIE); helper.assertEntityCount(EntityType.ZOMBIE, expectedCount); helper.assertEntityProperty(entity, entity -> entity.getHealth() > 0, "alive"); // Item assertions helper.assertContainerContains(pos, Items.DIAMOND); helper.assertContainerEmpty(pos); // Control flow helper.succeed(); // mark test as passed — REQUIRED at end helper.fail("reason"); // mark test as failed helper.runAfterDelay(ticks, runnable); // schedule assertion helper.onEachTick(runnable); // run every tick (use with care) helper.succeedWhen(() -> { /* assertions */ }); // poll until assertions pass or timeout helper.succeedOnTickWhen(tick, () -> { /* assertions */ });
CI: Running Tests in GitHub Actions
# .github/workflows/test.yml name: Tests on: [push, pull_request] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - uses: gradle/actions/setup-gradle@v3 - name: Run unit tests run: ./gradlew test game-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - uses: gradle/actions/setup-gradle@v3 - name: Run GameTests (headless) run: ./gradlew runGameTestServer env: # Required for headless rendering CI: true
References
- MockBukkit GitHub: https://github.com/MockBukkit/MockBukkit
- MockBukkit docs: https://mockbukkit.readthedocs.io/
- NeoForge GameTest docs: https://docs.neoforged.net/docs/misc/gametest/
- Fabric GameTest API: https://wiki.fabricmc.net/tutorial:gametests
- JUnit 5 user guide: https://junit.org/junit5/docs/current/user-guide/