minecraft-testing-25be5b — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited minecraft-testing-25be5b (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| 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) |
Use when: the task is designing or implementing automated tests (unit, mock, gametest, CI test jobs) for Minecraft projects.Do not use when: the task is implementing gameplay features rather than testing them (minecraft-modding, minecraft-plugin-dev, minecraft-datapack).Do not use when: the task is release automation or publishing pipelines (minecraft-ci-release).references/test-layouts.md./scripts/validate-test-layout.sh --root <project>Use the validator before copying a test layout into a real project. It checks for the common breakpoints that show up in 1.21.x plugin/mod test repos: missing useJUnitPlatform(), MockBukkit tests without the dependency, GameTests with missing committed template files, and missing NeoForge/Fabric GameTest registration metadata.
build.gradle.kts additionsdependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}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));
}
}build.gradle.ktsrepositories {
maven("https://repo.papermc.io/repository/maven-public/")
mavenCentral()
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.0.0")
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());
}MockBukkit does not emulate Folia's region-threaded runtime. The safe pattern is to wrap scheduling behind your own interface and unit test the abstraction boundary.
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);
}@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);
}@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);
}GameTests run inside a Minecraft world. They place a structure (the test environment), then run assertions using GameTestHelper.
// In your mod main class:
@Mod(MyMod.MOD_ID)
public class MyMod {
public MyMod(IEventBus modEventBus) {
modEventBus.register(MyGameTests.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();
});
}
}.nbt files)Place 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, and keep the namespace/path aligned with each literal @GameTest(template = "mymod:...") value so the validator can catch missing templates before runtime.
.nbt structure files exist at src/main/resources/data/<modid>/structures/modEventBus.register(MyGameTests.class))./gradlew runGameTestServer — if tests fail with "Missing template", the .nbt file path or name is wrongPASSED/FAILED per testtimeoutTicks in the @GameTest annotation or add intermediate assertions with runAfterDelay./gradlew runGameTestServer
# In-game (dev environment):
# /test runall
# /test run mymod:test_block_interactionimport 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();
});
}
}fabric.mod.json{
"entrypoints": {
"fabric-gametest": [
"com.example.mymod.fabric.MyFabricGameTests"
]
}
}Keep the fabric-gametest entrypoint in sync with the concrete GameTest class name. The validator checks both the metadata file and the entry itself.
GameTestHelper Assertions Reference// 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 */ });Split CI into fast unit/mock coverage and slower runtime-facing jobs. MockBukkit is great for command/event logic, but it does not prove Folia thread safety or real server bootstrap.
# .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@v4
- { 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@v4
- { name: Run GameTests (headless), run: ./gradlew runGameTestServer, env: { CI: true } }
layout-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- { name: Validate test layout, run: ./scripts/validate-test-layout.sh --root . }~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.