Codex CLI Claude Skill

minecraft-testing

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.

LLM Mart · 0 points · 7 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Jahrome907-minecraft-agent-skills-.codex_skills_minecraft-testing-dd57c5a.zip · 10 KB
Part of jahrome907/minecraft-agent-skills — 52 skills

Install

skills CLI npx skills add https://github.com/Jahrome907/minecraft-agent-skills/tree/main/.codex/skills/minecraft-testing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jahrome907-minecraft-agent-skills@llmmart
Git git clone https://github.com/Jahrome907/minecraft-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jahrome907/minecraft-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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)

Use Java 25 for current 26.x projects. Keep legacy 1.21.x examples on Java 21 and Forge 1.20.1 on Java 17. Do not combine source layouts or APIs across versions.

Routing Boundaries

  • 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).

Bundled References And Helpers

  • Layout guide: references/test-layouts.md
  • Fixture/layout validator: ./scripts/validate-test-layout.sh --root <project>

Use the validator before copying a test layout into a real project. It checks visible static dependencies, metadata, and literal structure references. It does not compile the project or prove a Game Test can run.


Unit Testing (JUnit 5 — No Minecraft)

JUnit Platform task

tasks.test {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}

For Fabric code that needs loader setup, use Fabric Loader JUnit rather than assuming ordinary JUnit initialized Minecraft:

dependencies {
    testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
}

test {
    useJUnitPlatform()
}

When a unit test reaches registry-dependent Minecraft classes, initialize only the required bootstrap in test setup. The current Fabric guide uses SharedConstants.tryDetectVersion() and Bootstrap.bootStrap() for that case.

Example pure unit test

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));
    }
}

MockBukkit (Paper/Bukkit Plugin Tests)

build.gradle.kts

repositories {
    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()
}

Setup / teardown pattern

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();
    }
}

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 Folia-safe scheduler abstractions

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);
}

Testing PDC

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);
}

This dispatches a synthetic death event. For player attribution, the listener under test should read event.getDamageSource().getCausingEntity(); test actual combat attribution separately on a real server.

Testing item or chunk PDC writes

@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);
}

Current NeoForge Game Tests (26.x)

NeoForge 1.21.5 and later uses data-driven test environments and test instances, not the old @GameTestHolder method-registration API. Store resources under data/<namespace>/test_environment/ and data/<namespace>/test_instance/. A test_instance selects its environment, structure, timing, and either a registered function or a block-based test.

{
  "environment": "minecraft:default",
  "structure": "examplemod:example_structure",
  "max_ticks": 200,
  "setup_ticks": 0,
  "required": true,
  "type": "minecraft:function",
  "function": "examplemod:example_function"
}

Register the Consumer<GameTestHelper> with a DeferredRegister for the current BuiltInRegistries.TEST_FUNCTION registry, then attach that register to the mod event bus. The function below makes the JSON reference above usable. Use RegisterGameTestsEvent only when registering environments and test instances in code instead of data files. Keep the referenced structure in data/<namespace>/structure/<path>.nbt and mark success explicitly.

import 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();
    }
}

Run ./gradlew runGameTestServer; the server exits with the count of required failed tests. This command is for a real project, not this skills repository.


Current Fabric Game Tests (26.x)

Use Fabric Loom's dedicated Game Test source set. Configure it in the existing fabricApi block and keep its metadata and code under src/gametest, separate from ordinary unit tests.

fabricApi {
    configureTests {
        createSourceSet = true
        modId = "example-mod-test-${project.name}"
        enableGameTests = true
        enableClientGameTests = true
        eula = true
    }
}

Place fabric.mod.json in src/gametest/resources/ and register server tests under fabric-gametest; use fabric-client-gametest for client tests. Implement server methods with Fabric's net.fabricmc.fabric.api.gametest.v1.GameTest and, when setup is needed before a method runs, CustomTestMethodInvoker.

package 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"
    ]
  }
}

Keep the fabric-gametest entrypoint in sync with the concrete Game Test class. Fabric's server Game Tests run with build; use runClientGameTest for client tests. Follow the current Fabric documentation for project-specific Loom options and headless client CI.


Legacy NeoForge Game Tests (1.21.3 only)

Keep annotation-based tests isolated to Minecraft 1.21.3. The class can be registered by either @GameTestHolder(MOD_ID) or a RegisterGameTestsEvent listener. Do not register a @GameTestHolder class again with modEventBus.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();
    }
}

For RegisterGameTestsEvent, register the class on the mod event bus and set templateNamespace = MOD_ID on each @GameTest. Legacy templates are .nbt files under data/<namespace>/structure/; @PrefixGameTestTemplate(false) controls whether the class name is added to the template path. When template is omitted, the path uses the lowercase method name and, unless that prefix is disabled, the lowercase simple class name followed by a dot. template is the path name only; configure its namespace through templateNamespace or @GameTestHolder.


CI

Keep fast unit/mock tests separate from a loader's Game Test task, and select the Java version for each Minecraft version: 25 for 26.x, 21 for 1.21.x, and 17 for Forge 1.20.1. Upload test reports when a runtime-facing job fails. Do not assume a task name from another loader: Fabric server Game Tests run with build, while NeoForge uses runGameTestServer. MockBukkit does not prove Folia thread safety or real server bootstrap.


References

Files (minecraft-agent-skills)
  • references
    • test-layouts.md 3 KB
      # Minecraft Testing Layouts
      
      Choose the target platform and version. `src/test` is for JUnit or
      MockBukkit; it is optional for a Game Test-only project.
      
      ## Unit + MockBukkit plugin
      
      ```text
      src/
        main/
          java/com/example/myplugin/
          resources/
            plugin.yml
        test/
          java/com/example/myplugin/
            MyPluginTest.java
            CommandExecutorTest.java
      ```
      
      Checklist:
      
      - `build.gradle(.kts)` declares compatible JUnit Jupiter and MockBukkit versions
      - `tasks.test { useJUnitPlatform() }` is enabled
      
      ## Current Fabric 26.x
      
      Fabric Loom's recommended Game Test layout uses a separate source set.
      
      ```text
      src/
        main/
          java/com/example/mymod/
          resources/
            fabric.mod.json
        gametest/
          java/com/example/mymod/
            ExampleGameTest.java
          resources/
            fabric.mod.json
            data/mymod/structure/
              example_structure.nbt
        test/                         # only when JUnit tests exist
          java/com/example/mymod/
            SerializerTest.java
      ```
      
      Configure `fabricApi.configureTests { createSourceSet = true }`. Register server
      tests in `src/gametest/resources/fabric.mod.json` under `fabric-gametest`, and
      client tests under `fabric-client-gametest`. The current Fabric API annotation
      is `net.fabricmc.fabric.api.gametest.v1.GameTest`.
      
      ## Current NeoForge 26.x
      
      ```text
      src/
        main/
          java/com/example/mymod/
            GameTestFunctions.java
          resources/
            META-INF/neoforge.mods.toml
            data/mymod/structure/
              empty.nbt
            data/mymod/test_instance/
              example_test.json
        test/                         # only when JUnit tests exist
          java/com/example/mymod/
            CooldownManagerTest.java
      ```
      
      Checklist:
      
      NeoForge 1.21.5+ models Game Tests as registered test environments, functions,
      and test instances. A `test_instance` must reference an existing structure. Add
      a `test_environment` resource when `minecraft:default` is not sufficient.
      Register custom test functions with a `DeferredRegister` for
      `BuiltInRegistries.TEST_FUNCTION` and attach it to the mod event bus. Use
      `RegisterGameTestsEvent` to register environments and instances in code.
      
      ## Legacy NeoForge 1.21.3
      
      Only use the annotation route for a clearly labelled 1.21.3 project.
      
      ```text
      src/
        main/
          java/com/example/mymod/
            LegacyGameTests.java
          resources/
            META-INF/neoforge.mods.toml
            data/mymod/structure/
              example_structure.nbt
      ```
      
      `@GameTestHolder(MOD_ID)` registers the methods in the annotated type. The
      alternative is an event-bus `RegisterGameTestsEvent` listener that calls
      `event.register(LegacyGameTests.class)`; then each test provides its
      `templateNamespace`. Do not require both mechanisms.
      
      ## Validator Usage
      
      ```bash
      ./scripts/validate-test-layout.sh --root .
      ./scripts/validate-test-layout.sh --root . --strict
      ```
      
      What it checks:
      
      - build file exists
      - JUnit Platform is enabled when unit or MockBukkit tests are present
      - MockBukkit tests have the dependency
      - GameTests have committed structure fixtures that match referenced templates
      - Fabric GameTests include their metadata and entrypoints
      
  • scripts
    • validate-test-layout.sh 14.9 KB
      #!/usr/bin/env bash
      set -euo pipefail
      
      PASS='[PASS]'
      WARN='[WARN]'
      FAIL='[FAIL]'
      
      ROOT='.'
      STRICT=0
      
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --root)
            ROOT="${2:-}"
            shift 2
            ;;
          --strict)
            STRICT=1
            shift
            ;;
          --help|-h)
            cat <<'USAGE'
      Usage: validate-test-layout.sh [--root <path>] [--strict]
      
      Checks common Minecraft testing layout expectations:
      - build.gradle(.kts) exists
      - unit or MockBukkit tests enable JUnit Platform
      - MockBukkit tests have the MockBukkit dependency
      - GameTests have committed structure fixtures that match referenced templates
      - Fabric GameTests include their required registration metadata
      USAGE
            exit 0
            ;;
          *)
            echo "$FAIL unknown arg: $1" >&2
            exit 1
            ;;
        esac
      done
      
      if [[ ! -d "$ROOT" ]]; then
        echo "$FAIL root path does not exist: $ROOT" >&2
        exit 1
      fi
      
      FAILURES=0
      WARNINGS=0
      
      pass() { echo "$PASS $*"; }
      warn() { echo "$WARN $*"; WARNINGS=$((WARNINGS + 1)); }
      fail() { echo "$FAIL $*"; FAILURES=$((FAILURES + 1)); }
      
      extract_package_name() {
        local file="$1"
        awk '/^[[:space:]]*package[[:space:]]+/ { gsub(/;/, "", $2); print $2; exit }' "$file"
      }
      
      extract_class_name() {
        local file="$1"
        sed -nE 's/.*(^|[[:space:]])(class|object)[[:space:]]+([A-Za-z_][A-Za-z0-9_]*).*/\3/p' "$file" | head -n 1
      }
      
      extract_fqcn() {
        local file="$1"
        local package_name
        local class_name
        package_name="$(extract_package_name "$file")"
        class_name="$(extract_class_name "$file")"
      
        if [[ -z "$class_name" ]]; then
          return 1
        fi
      
        if [[ -n "$package_name" ]]; then
          printf '%s.%s\n' "$package_name" "$class_name"
        else
          printf '%s\n' "$class_name"
        fi
      }
      
      template_fixture_exists() {
        local root="$1"
        local namespace="$2"
        local path="$3"
        local structure_roots=()
      
        if [[ -d "$root/src/test/resources" ]]; then
          structure_roots+=("$root/src/test/resources")
        fi
        if [[ -d "$root/src/main/resources" ]]; then
          structure_roots+=("$root/src/main/resources")
        fi
        if [[ -d "$root/src/gametest/resources" ]]; then
          structure_roots+=("$root/src/gametest/resources")
        fi
      
        if [[ "${#structure_roots[@]}" -eq 0 ]]; then
          return 1
        fi
      
        local structure_rel="data/$namespace/structure/$path.nbt"
        local structure_root
        for structure_root in "${structure_roots[@]}"; do
          if [[ -f "$structure_root/$structure_rel" ]]; then
            return 0
          fi
        done
      
        return 1
      }
      
      append_legacy_neoforge_implicit_templates() {
        local source_file="$1"
        local class_name="$2"
        local holder_namespace=''
        local holder_present=0
        local class_prefix_disabled=0
        local line
        local annotation
        local method
        local method_line
        local namespace
        local path
        local template_name
        local method_prefix_disabled
        local index
        local candidate_index
        local lookahead_index
        local -a source_lines=()
      
        if grep -E -q '@GameTestHolder[[:space:]]*\(' "$source_file"; then
          holder_present=1
        fi
        holder_namespace="$({ grep -oE '@GameTestHolder[[:space:]]*\([[:space:]]*"[^"]+' "$source_file" || true; } | head -n 1 | sed -E 's/.*"//')"
        mapfile -t source_lines < "$source_file"
      
        for ((index = 0; index < ${#source_lines[@]}; index++)); do
          if [[ "${source_lines[$index]}" =~ @PrefixGameTestTemplate[[:space:]]*\([[:space:]]*false[[:space:]]*\) ]]; then
            for ((lookahead_index = index + 1; lookahead_index < ${#source_lines[@]}; lookahead_index++)); do
              line="${source_lines[$lookahead_index]}"
              if [[ "$line" =~ (^|[[:space:]])(class|object)[[:space:]]+ ]]; then
                class_prefix_disabled=1
                break 2
              fi
              if [[ "$line" =~ @GameTest([[:space:]]|\(|$) ]]; then
                break
              fi
            done
          fi
        done
      
        for ((index = 0; index < ${#source_lines[@]}; index++)); do
          line="${source_lines[$index]}"
          [[ "$line" =~ @GameTest([[:space:]]|\(|$) ]] || continue
      
          annotation="$line"
          method=''
          method_prefix_disabled=0
          if (( index > 0 )) && [[ "${source_lines[$((index - 1))]}" =~ @PrefixGameTestTemplate[[:space:]]*\([[:space:]]*false[[:space:]]*\) ]]; then
            method_prefix_disabled=1
          fi
      
          for ((candidate_index = index; candidate_index < ${#source_lines[@]}; candidate_index++)); do
            method_line="${source_lines[$candidate_index]}"
            if (( candidate_index > index )); then
              annotation+=" $method_line"
            fi
            if [[ "$method_line" =~ (^|[[:space:]])(public|protected|private|internal|static)[[:space:]].*\( || "$method_line" =~ (^|[[:space:]])fun[[:space:]]+ ]]; then
              if [[ "$method_line" =~ ([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*\( ]]; then
                method="${BASH_REMATCH[1]}"
              fi
              break
            fi
          done
      
          [[ -n "$method" ]] || continue
      
          # Java annotations may appear in either order before the method. The
          # collected annotation block covers both @PrefixGameTestTemplate(false)
          # before and after @GameTest.
          if [[ "$annotation" =~ @PrefixGameTestTemplate[[:space:]]*\([[:space:]]*false[[:space:]]*\) ]]; then
            method_prefix_disabled=1
          fi
      
          template_name="$(printf '%s' "$method" | tr '[:upper:]' '[:lower:]')"
          if [[ "$annotation" =~ template[[:space:]]*= ]]; then
            if [[ "$annotation" =~ template[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
              template_name="${BASH_REMATCH[1]}"
            else
              warn "legacy NeoForge GameTest has a non-literal template name; fixture path not verified: ${source_file#$ROOT/}"
              continue
            fi
          fi
          if [[ "$template_name" == *:* ]]; then
            warn "legacy NeoForge GameTest template must be an un-namespaced name; configure its namespace with templateNamespace or @GameTestHolder: ${source_file#$ROOT/}"
            continue
          fi
      
          namespace="$holder_namespace"
          if [[ "$annotation" =~ templateNamespace[[:space:]]*= ]]; then
            if [[ "$annotation" =~ templateNamespace[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
              namespace="${BASH_REMATCH[1]}"
            else
              warn "legacy NeoForge GameTest has a non-literal templateNamespace; fixture path not verified: ${source_file#$ROOT/}"
              continue
            fi
          elif [[ "$holder_present" -eq 1 && -z "$namespace" ]]; then
            warn "legacy NeoForge GameTest has a non-literal @GameTestHolder value; fixture path not verified: ${source_file#$ROOT/}"
            continue
          elif [[ -z "$namespace" ]]; then
            namespace='minecraft'
          fi
      
          path="$template_name"
          if [[ "$class_prefix_disabled" -eq 0 && "$method_prefix_disabled" -eq 0 ]]; then
            path="$(printf '%s' "$class_name" | tr '[:upper:]' '[:lower:]').$path"
          fi
          GAME_TEST_TEMPLATES+=("$namespace:$path")
        done
      }
      
      legacy_neoforge_event_registers_class() {
        local root="$1"
        local class_name="$2"
        local source_file
        local event_variable
      
        while IFS= read -r -d '' source_file; do
          while IFS= read -r event_variable; do
            if grep -E -q "${event_variable}[[:space:]]*\\.[[:space:]]*register[[:space:]]*\\([[:space:]]*([A-Za-z_][A-Za-z0-9_]*\\.)*${class_name}\\.class[[:space:]]*\\)" "$source_file"; then
              return 0
            fi
          done < <(grep -oE 'RegisterGameTestsEvent[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' "$source_file" | sed -E 's/.*[[:space:]]([A-Za-z_][A-Za-z0-9_]*)$/\1/')
        done < <(find "$root/src/main" "$root/src/test" -type f \( -name '*.java' -o -name '*.kt' \) -print0 2>/dev/null)
      
        return 1
      }
      
      BUILD_FILE=''
      if [[ -f "$ROOT/build.gradle.kts" ]]; then
        BUILD_FILE="$ROOT/build.gradle.kts"
      elif [[ -f "$ROOT/build.gradle" ]]; then
        BUILD_FILE="$ROOT/build.gradle"
      else
        fail "missing build.gradle or build.gradle.kts"
      fi
      
      TEST_ROOT=''
      if [[ -d "$ROOT/src/test/java" ]]; then
        TEST_ROOT="$ROOT/src/test/java"
      elif [[ -d "$ROOT/src/test/kotlin" ]]; then
        TEST_ROOT="$ROOT/src/test/kotlin"
      fi
      
      if [[ -n "$BUILD_FILE" ]]; then
        pass "found build file: ${BUILD_FILE#$ROOT/}"
      fi
      
      if [[ -n "$TEST_ROOT" ]]; then
        pass "found test source root: ${TEST_ROOT#$ROOT/}"
      fi
      
      HAS_MOCKBUKKIT_TESTS=0
      if [[ -n "$TEST_ROOT" ]] && grep -R -E -q 'MockBukkit|ServerMock|PlayerMock' "$TEST_ROOT"; then
        HAS_MOCKBUKKIT_TESTS=1
        pass "MockBukkit-style tests detected"
      fi
      
      if [[ "$HAS_MOCKBUKKIT_TESTS" -eq 1 && -n "$BUILD_FILE" ]]; then
        if grep -R -E -q 'be\.seeseemelk|com\.github\.seeseemelk' "$BUILD_FILE" "$TEST_ROOT"; then
          warn "legacy MockBukkit 3.x coordinate or package detected; prefer org.mockbukkit.mockbukkit 4.x"
        fi
      
        if grep -Eiq 'MockBukkit|mockbukkit' "$BUILD_FILE"; then
          pass "build file declares MockBukkit dependency"
        else
          fail "MockBukkit tests detected but build file is missing MockBukkit dependency"
        fi
      fi
      
      HAS_GAMETESTS=0
      declare -a SOURCE_SCAN_ROOTS=()
      for candidate_root in \
        "$ROOT/src/main/java" \
        "$ROOT/src/main/kotlin" \
        "$ROOT/src/test/java" \
        "$ROOT/src/test/kotlin" \
        "$ROOT/src/gametest/java" \
        "$ROOT/src/gametest/kotlin"; do
        if [[ -d "$candidate_root" ]]; then
          SOURCE_SCAN_ROOTS+=("$candidate_root")
        fi
      done
      
      declare -a GAME_TEST_FILES=()
      declare -a GAME_TEST_TEMPLATES=()
      declare -a NEOFORGE_GAMETEST_CLASSES=()
      declare -a NEOFORGE_EVENT_REGISTERED_CLASSES=()
      declare -a FABRIC_GAMETEST_CLASSES=()
      
      if [[ "${#SOURCE_SCAN_ROOTS[@]}" -gt 0 ]]; then
        while IFS= read -r -d '' source_file; do
          if grep -E -q '@GameTest|FabricGameTest|GameTestHelper' "$source_file"; then
            GAME_TEST_FILES+=("$source_file")
            fqcn="$(extract_fqcn "$source_file" || true)"
            if [[ -n "$fqcn" ]]; then
              # Only the 1.21.3 annotation API needs holder or event registration.
              # Current data-driven NeoForge test-function classes also use
              # GameTestHelper, but have no @GameTest annotation to register.
              if grep -E -q '@GameTest([[:space:]]|\(|$)' "$source_file" \
                && { grep -E -q 'net\.neoforged|@GameTestHolder|PrefixGameTestTemplate' "$source_file" \
                  || { [[ -f "$ROOT/src/main/resources/META-INF/neoforge.mods.toml" ]] && ! grep -E -q 'FabricGameTest|fabric\.api\.gametest' "$source_file"; }; }; then
                NEOFORGE_GAMETEST_CLASSES+=("$fqcn")
                if ! grep -E -q '@GameTestHolder' "$source_file"; then
                  NEOFORGE_EVENT_REGISTERED_CLASSES+=("$fqcn")
                fi
                append_legacy_neoforge_implicit_templates "$source_file" "${fqcn##*.}"
              else
                while IFS= read -r template; do
                  [[ -n "$template" ]] && GAME_TEST_TEMPLATES+=("$template")
                done < <(grep -oE '@GameTest\([^)]*template[[:space:]]*=[[:space:]]*"[^"]+"' "$source_file" | sed -E 's/.*template[[:space:]]*=[[:space:]]*"([^"]+)"/\1/')
              fi
              if grep -E -q 'FabricGameTest|fabric\.api\.gametest' "$source_file"; then
                FABRIC_GAMETEST_CLASSES+=("$fqcn")
              fi
            fi
          fi
        done < <(find "${SOURCE_SCAN_ROOTS[@]}" -type f \( -name '*.java' -o -name '*.kt' \) -print0)
      fi
      
      declare -a RESOURCE_SCAN_ROOTS=()
      for candidate_root in \
        "$ROOT/src/main/resources" \
        "$ROOT/src/test/resources" \
        "$ROOT/src/gametest/resources"; do
        if [[ -d "$candidate_root" ]]; then
          RESOURCE_SCAN_ROOTS+=("$candidate_root")
        fi
      done
      
      HAS_CURRENT_NEOFORGE_GAMETESTS=0
      if [[ "${#RESOURCE_SCAN_ROOTS[@]}" -gt 0 ]]; then
        while IFS= read -r -d '' instance_file; do
          HAS_CURRENT_NEOFORGE_GAMETESTS=1
          while IFS= read -r template; do
            [[ -n "$template" ]] && GAME_TEST_TEMPLATES+=("$template")
          done < <(grep -oE '"structure"[[:space:]]*:[[:space:]]*"[^"]+"' "$instance_file" | sed -E 's/.*"structure"[[:space:]]*:[[:space:]]*"([^"]+)"/\1/')
        done < <(find "${RESOURCE_SCAN_ROOTS[@]}" -type f -path '*/data/*/test_instance/*.json' -print0)
      fi
      
      HAS_JUNIT_STYLE_TESTS=0
      if [[ -n "$TEST_ROOT" ]] && grep -R -E -q 'org\.junit|@Test|@ParameterizedTest' "$TEST_ROOT"; then
        HAS_JUNIT_STYLE_TESTS=1
      fi
      
      if [[ "$HAS_JUNIT_STYLE_TESTS" -eq 1 || "$HAS_MOCKBUKKIT_TESTS" -eq 1 ]]; then
        if grep -Eq 'useJUnitPlatform' "$BUILD_FILE"; then
          pass "test task enables JUnit Platform"
        else
          fail "unit or MockBukkit tests require useJUnitPlatform()"
        fi
      fi
      
      if [[ "${#GAME_TEST_FILES[@]}" -gt 0 || "$HAS_CURRENT_NEOFORGE_GAMETESTS" -eq 1 ]]; then
        HAS_GAMETESTS=1
        pass "GameTest-style tests detected"
      fi
      
      if [[ "$HAS_GAMETESTS" -eq 1 ]]; then
        if [[ "${#GAME_TEST_TEMPLATES[@]}" -gt 0 ]]; then
          for template in "${GAME_TEST_TEMPLATES[@]}"; do
            if [[ "$template" =~ ^([a-z0-9_.-]+):([a-z0-9_./-]+)$ ]]; then
              if template_fixture_exists "$ROOT" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; then
                pass "GameTest template fixture exists: $template"
              else
                fail "GameTest template fixture missing: $template"
              fi
            else
              warn "GameTest template uses a non-literal or unsupported format: $template"
            fi
          done
        else
          pass "no literal GameTest structure reference found; skipping fixture-path validation"
        fi
      
        if [[ "${#NEOFORGE_GAMETEST_CLASSES[@]}" -gt 0 || "$HAS_CURRENT_NEOFORGE_GAMETESTS" -eq 1 ]]; then
          if [[ -f "$ROOT/src/main/resources/META-INF/neoforge.mods.toml" ]]; then
            pass "NeoForge metadata found for GameTests"
          else
            fail "NeoForge GameTests detected but src/main/resources/META-INF/neoforge.mods.toml is missing"
          fi
      
          for fqcn in "${NEOFORGE_EVENT_REGISTERED_CLASSES[@]}"; do
            class_name="${fqcn##*.}"
            if legacy_neoforge_event_registers_class "$ROOT" "$class_name"; then
              pass "legacy NeoForge GameTest class has an event registration path: $fqcn"
            else
              fail "legacy NeoForge GameTest class needs @GameTestHolder or RegisterGameTestsEvent registration: $fqcn"
            fi
          done
        fi
      
        if [[ "${#FABRIC_GAMETEST_CLASSES[@]}" -gt 0 ]]; then
          FABRIC_MOD_JSON=''
          if [[ -f "$ROOT/src/gametest/resources/fabric.mod.json" ]]; then
            FABRIC_MOD_JSON="$ROOT/src/gametest/resources/fabric.mod.json"
          elif [[ -f "$ROOT/src/main/resources/fabric.mod.json" ]]; then
            FABRIC_MOD_JSON="$ROOT/src/main/resources/fabric.mod.json"
          fi
          if [[ -f "$FABRIC_MOD_JSON" ]]; then
            pass "Fabric metadata found for GameTests"
            if grep -Fq '"fabric-gametest"' "$FABRIC_MOD_JSON"; then
              pass "fabric.mod.json declares fabric-gametest entrypoints"
            else
              fail "fabric.mod.json is missing the fabric-gametest entrypoint block"
            fi
      
            for fqcn in "${FABRIC_GAMETEST_CLASSES[@]}"; do
              if grep -Fq "$fqcn" "$FABRIC_MOD_JSON"; then
                pass "fabric.mod.json registers GameTest entrypoint: $fqcn"
              else
                fail "fabric.mod.json is missing the fabric-gametest entry for $fqcn"
              fi
            done
          else
            if [[ -d "$ROOT/src/gametest" ]]; then
              fail "Fabric GameTests detected but src/gametest/resources/fabric.mod.json is missing"
            else
              fail "Fabric GameTests detected but src/main/resources/fabric.mod.json is missing"
            fi
          fi
        fi
      fi
      
      if [[ "$HAS_MOCKBUKKIT_TESTS" -eq 0 && "$HAS_GAMETESTS" -eq 0 ]]; then
        warn "no MockBukkit or GameTest fixtures detected; layout only covers plain unit tests"
      fi
      
      echo ""
      if [[ "$FAILURES" -gt 0 ]]; then
        echo "$FAIL testing layout validation failed with $FAILURES error(s) and $WARNINGS warning(s)"
        exit 1
      fi
      
      if [[ "$STRICT" -eq 1 && "$WARNINGS" -gt 0 ]]; then
        echo "$FAIL testing layout validation strict mode failed on $WARNINGS warning(s)"
        exit 1
      fi
      
      echo "$PASS testing layout validation passed with $WARNINGS warning(s)"
      
  • SKILL.md 15 KB
    ---
    name: minecraft-testing
    description: "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."
    ---
    
    # 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) |
    
    Use Java 25 for current 26.x projects. Keep legacy 1.21.x examples on Java 21
    and Forge 1.20.1 on Java 17. Do not combine source layouts or APIs across versions.
    
    ### Routing Boundaries
    - `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`).
    
    ## Bundled References And Helpers
    
    - Layout guide: `references/test-layouts.md`
    - Fixture/layout validator: `./scripts/validate-test-layout.sh --root <project>`
    
    Use the validator before copying a test layout into a real project. It checks
    visible static dependencies, metadata, and literal structure references. It does
    not compile the project or prove a Game Test can run.
    
    ---
    
    ## Unit Testing (JUnit 5 — No Minecraft)
    
    ### JUnit Platform task
    ```kotlin
    tasks.test {
        useJUnitPlatform()
        testLogging {
            events("passed", "skipped", "failed")
        }
    }
    ```
    
    For Fabric code that needs loader setup, use Fabric Loader JUnit rather than
    assuming ordinary JUnit initialized Minecraft:
    
    ```groovy
    dependencies {
        testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
    }
    
    test {
        useJUnitPlatform()
    }
    ```
    
    When a unit test reaches registry-dependent Minecraft classes, initialize only
    the required bootstrap in test setup. The current Fabric guide uses
    `SharedConstants.tryDetectVersion()` and `Bootstrap.bootStrap()` for that case.
    
    ### Example pure unit test
    ```java
    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));
        }
    }
    ```
    
    ---
    
    ## MockBukkit (Paper/Bukkit Plugin Tests)
    
    ### `build.gradle.kts`
    ```kotlin
    repositories {
        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()
    }
    ```
    
    ### Setup / teardown pattern
    ```java
    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();
        }
    }
    ```
    
    ### Testing events
    ```java
    @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
    ```java
    @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
    ```java
    @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
    ```java
    @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 Folia-safe scheduler abstractions
    
    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.
    
    ```java
    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);
    }
    ```
    
    ### Testing PDC
    ```java
    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);
    }
    ```
    
    This dispatches a synthetic death event. For player attribution, the listener
    under test should read `event.getDamageSource().getCausingEntity()`; test actual
    combat attribution separately on a real server.
    
    ### Testing item or chunk PDC writes
    ```java
    @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);
    }
    ```
    
    ---
    
    ## Current NeoForge Game Tests (26.x)
    
    NeoForge 1.21.5 and later uses data-driven test environments and test instances,
    not the old `@GameTestHolder` method-registration API. Store resources under
    `data/<namespace>/test_environment/` and `data/<namespace>/test_instance/`.
    A `test_instance` selects its environment, structure, timing, and either a
    registered function or a block-based test.
    
    ```json
    {
      "environment": "minecraft:default",
      "structure": "examplemod:example_structure",
      "max_ticks": 200,
      "setup_ticks": 0,
      "required": true,
      "type": "minecraft:function",
      "function": "examplemod:example_function"
    }
    ```
    
    Register the `Consumer<GameTestHelper>` with a `DeferredRegister` for the
    current `BuiltInRegistries.TEST_FUNCTION` registry, then attach that register to
    the mod event bus. The function below makes the JSON reference above usable.
    Use `RegisterGameTestsEvent` only when registering environments and test
    instances in code instead of data files. Keep the referenced structure in
    `data/<namespace>/structure/<path>.nbt` and mark success explicitly.
    
    ```java
    import 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();
        }
    }
    ```
    
    Run `./gradlew runGameTestServer`; the server exits with the count of required
    failed tests. This command is for a real project, not this skills repository.
    
    ---
    
    ## Current Fabric Game Tests (26.x)
    
    Use Fabric Loom's dedicated Game Test source set. Configure it in the existing
    `fabricApi` block and keep its metadata and code under `src/gametest`, separate
    from ordinary unit tests.
    
    ```groovy
    fabricApi {
        configureTests {
            createSourceSet = true
            modId = "example-mod-test-${project.name}"
            enableGameTests = true
            enableClientGameTests = true
            eula = true
        }
    }
    ```
    
    Place `fabric.mod.json` in `src/gametest/resources/` and register server tests
    under `fabric-gametest`; use `fabric-client-gametest` for client tests. Implement
    server methods with Fabric's `net.fabricmc.fabric.api.gametest.v1.GameTest` and,
    when setup is needed before a method runs, `CustomTestMethodInvoker`.
    
    ```java
    package 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`
    ```json
    {
      "entrypoints": {
        "fabric-gametest": [
          "com.example.mymod.ExampleGameTest"
        ]
      }
    }
    ```
    
    Keep the `fabric-gametest` entrypoint in sync with the concrete Game Test class.
    Fabric's server Game Tests run with `build`; use `runClientGameTest` for client
    tests. Follow the current Fabric documentation for project-specific Loom options
    and headless client CI.
    
    ---
    
    ## Legacy NeoForge Game Tests (1.21.3 only)
    
    Keep annotation-based tests isolated to Minecraft 1.21.3. The class can
    be registered by either `@GameTestHolder(MOD_ID)` or a
    `RegisterGameTestsEvent` listener. Do not register a `@GameTestHolder` class
    again with `modEventBus.register(MyGameTests.class)`.
    
    ```java
    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();
        }
    }
    ```
    
    For `RegisterGameTestsEvent`, register the class on the mod event bus and set
    `templateNamespace = MOD_ID` on each `@GameTest`. Legacy templates are `.nbt`
    files under `data/<namespace>/structure/`; `@PrefixGameTestTemplate(false)`
    controls whether the class name is added to the template path. When `template`
    is omitted, the path uses the lowercase method name and, unless that prefix is
    disabled, the lowercase simple class name followed by a dot. `template` is the
    path name only; configure its namespace through `templateNamespace` or
    `@GameTestHolder`.
    
    ---
    
    ## CI
    
    Keep fast unit/mock tests separate from a loader's Game Test task, and select
    the Java version for each Minecraft version: 25 for 26.x, 21 for 1.21.x, and 17 for Forge 1.20.1.
    Upload test reports when a runtime-facing job fails. Do not assume a task name
    from another loader: Fabric server Game Tests run with `build`, while NeoForge
    uses `runGameTestServer`. MockBukkit does not prove Folia thread safety or real
    server bootstrap.
    
    ---
    
    ## References
    
    - MockBukkit GitHub: https://github.com/MockBukkit/MockBukkit
    - MockBukkit docs: https://docs.mockbukkit.org/
    - Fabric automated testing: https://docs.fabricmc.net/develop/automatic-testing
    - NeoForge 26.x Game Tests: https://docs.neoforged.net/docs/misc/gametest/
    - NeoForge 1.21.3 Game Tests: https://docs.neoforged.net/docs/1.21.3/misc/gametest/
    - JUnit 5 user guide: https://junit.org/junit5/docs/current/user-guide/
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related