Loading...
Loading...
Design and implement automated tests for current Minecraft 26.x or legacy 1.21.x mods and plugins using JUnit, MockBukkit, NeoForge Game Tests, or Fabric Game Tests. Use for test code and test execution, not release publishing or gameplay implementation.
npx skill4agent add jahrome907/minecraft-agent-skills minecraft-testing| Approach | Best For | Requires Game? |
|---|---|---|
| JUnit Jupiter (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) |
Use whenDo not use whenminecraft-moddingminecraft-plugin-devminecraft-datapackDo not use whenminecraft-ci-releasereferences/test-layouts.md./scripts/validate-test-layout.sh --root <project>tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}dependencies {
testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
}
test {
useJUnitPlatform()
}SharedConstants.tryDetectVersion()Bootstrap.bootStrap()import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CooldownManagerTest {
@Test
void cooldown_tracksPlayersIndependently() {
var manager = new CooldownManager(500L); // 500ms cooldown
manager.startCooldown("steve");
assertTrue(manager.isOnCooldown("steve"));
assertFalse(manager.isOnCooldown("notExisting"));
}
@Test
void cooldown_throwsIllegalArgument_onNegativeDuration() {
assertThrows(IllegalArgumentException.class,
() -> new CooldownManager(-1L));
}
}build.gradle.ktsrepositories {
maven("https://repo.papermc.io/repository/maven-public/")
mavenCentral()
}
dependencies {
compileOnly("io.papermc.paper:paper-api:26.2.build.+")
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v26.2:4.116.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}import org.mockbukkit.mockbukkit.MockBukkit;
import org.mockbukkit.mockbukkit.ServerMock;
import org.mockbukkit.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();
}
}@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");
}@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.");
}@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));
}@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());
}interface SchedulerFacade {
void runPlayerTask(Player player, Runnable task);
void runAsync(Runnable task);
}
@Test
void playerTask_delegatesThroughFacade() {
List<String> calls = new ArrayList<>();
SchedulerFacade facade = new SchedulerFacade() {
@Override
public void runPlayerTask(Player player, Runnable task) {
calls.add("player");
task.run();
}
@Override
public void runAsync(Runnable task) {
calls.add("async");
task.run();
}
};
facade.runPlayerTask(server.addPlayer(), () -> calls.add("ran"));
assertEquals(List.of("player", "ran"), calls);
}import java.util.ArrayList;
import org.bukkit.damage.DamageSource;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.LivingEntity;
@Test
void pdcKillCount_incrementsOnKill() {
PlayerMock player = server.addPlayer();
NamespacedKey key = new NamespacedKey(plugin, "kills");
// EntityDeathEvent requires a living victim and an explicit damage source.
LivingEntity victim = (LivingEntity) server.addMockEntity(EntityType.ZOMBIE);
DamageSource damageSource = DamageSource.builder(DamageType.GENERIC)
.withCausingEntity(player)
.withDirectEntity(player)
.build();
EntityDeathEvent deathEvent = new EntityDeathEvent(
victim, damageSource, new ArrayList<>(), 0
);
server.getPluginManager().callEvent(deathEvent);
int kills = player.getPersistentDataContainer()
.getOrDefault(key, PersistentDataType.INTEGER, 0);
assertEquals(1, kills);
}event.getDamageSource().getCausingEntity()@Test
void itemPdc_roundTripsCustomId() {
NamespacedKey key = new NamespacedKey(plugin, "custom_id");
ItemStack item = new ItemStack(Material.STICK);
item.editMeta(meta -> meta.getPersistentDataContainer().set(
key, PersistentDataType.STRING, "wand"
));
String value = item.getItemMeta().getPersistentDataContainer()
.get(key, PersistentDataType.STRING);
assertEquals("wand", value);
}@GameTestHolderdata/<namespace>/test_environment/data/<namespace>/test_instance/test_instance{
"environment": "minecraft:default",
"structure": "examplemod:example_structure",
"max_ticks": 200,
"setup_ticks": 0,
"required": true,
"type": "minecraft:function",
"function": "examplemod:example_function"
}Consumer<GameTestHelper>DeferredRegisterBuiltInRegistries.TEST_FUNCTIONRegisterGameTestsEventdata/<namespace>/structure/<path>.nbtimport java.util.function.Consumer;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
@Mod(ExampleGameTests.MOD_ID)
public final class ExampleGameTests {
public static final String MOD_ID = "examplemod";
private static final DeferredRegister<Consumer<GameTestHelper>> TEST_FUNCTIONS =
DeferredRegister.create(BuiltInRegistries.TEST_FUNCTION, MOD_ID);
public static final DeferredHolder<Consumer<GameTestHelper>, Consumer<GameTestHelper>>
EXAMPLE_FUNCTION = TEST_FUNCTIONS.register(
"example_function", () -> ExampleGameTests::exampleTest
);
public ExampleGameTests(IEventBus modBus) {
TEST_FUNCTIONS.register(modBus);
}
public static void exampleTest(GameTestHelper helper) {
helper.assertBlockPresent(Blocks.AIR, 0, 0, 0);
helper.succeed();
}
}./gradlew runGameTestServerfabricApisrc/gametestfabricApi {
configureTests {
createSourceSet = true
modId = "example-mod-test-${project.name}"
enableGameTests = true
enableClientGameTests = true
eula = true
}
}fabric.mod.jsonsrc/gametest/resources/fabric-gametestfabric-client-gametestnet.fabricmc.fabric.api.gametest.v1.GameTestCustomTestMethodInvokerpackage com.example.mymod;
import java.lang.reflect.Method;
import net.fabricmc.fabric.api.gametest.v1.CustomTestMethodInvoker;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
public final class ExampleGameTest implements CustomTestMethodInvoker {
@GameTest
public void testBlock(GameTestHelper context) {
context.assertBlockPresent(Blocks.AIR, 0, 0, 0);
context.succeed();
}
@Override
public void invokeTestMethod(GameTestHelper context, Method method)
throws ReflectiveOperationException {
method.invoke(this, context);
}
}src/gametest/resources/fabric.mod.json{
"entrypoints": {
"fabric-gametest": [
"com.example.mymod.ExampleGameTest"
]
}
}fabric-gametestbuildrunClientGameTest@GameTestHolder(MOD_ID)RegisterGameTestsEvent@GameTestHoldermodEventBus.register(MyGameTests.class)import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@GameTestHolder("examplemod")
@PrefixGameTestTemplate(false)
public final class ExampleGameTests {
@GameTest(template = "example_structure")
public static void smoke(GameTestHelper helper) {
helper.succeed();
}
}RegisterGameTestsEventtemplateNamespace = MOD_ID@GameTest.nbtdata/<namespace>/structure/@PrefixGameTestTemplate(false)templatetemplatetemplateNamespace@GameTestHolderbuildrunGameTestServer