minecraft-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Minecraft Testing Skill

Minecraft 测试 Skill

Testing Strategies Overview

测试策略概览

ApproachBest ForRequires Game?
JUnit Jupiter (pure unit tests)Logic, data structures, NBT serializationNo
MockBukkitBukkit/Paper plugin events, commands, inventoryNo (mocked server)
NeoForge GameTestsIn-game block/entity/world interactionYes (test environment)
Fabric GameTestsIn-game block/entity/world interactionYes (test environment)
Integration serverFull plugin/mod lifecycleYes (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.
方法最适用场景是否需要游戏?
JUnit Jupiter(纯单元测试)逻辑、数据结构、NBT 序列化否
MockBukkitBukkit/Paper 插件事件、命令、物品栏否(模拟服务器)
NeoForge GameTests游戏内方块/实体/世界交互是(测试环境)
Fabric GameTests游戏内方块/实体/世界交互是(测试环境)
集成服务器完整的插件/模组生命周期是(专用测试服务器)
当前的 26.x 项目使用 Java 25。旧版 1.21.x 示例使用 Java 21,Forge 1.20.1 使用 Java 17。不要混用不同版本的源代码结构或 API。

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
    ).
  • Use when
    :任务是为 Minecraft 项目设计或实施自动化测试(单元测试、模拟测试、gametest、CI 测试任务)。
  • Do not use when
    :任务是实现玩法功能而非测试功能(
    minecraft-modding
    、
    minecraft-plugin-dev
    、
    minecraft-datapack
    )。
  • Do not use when
    :任务是版本自动化或发布流水线(
    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.

  • 布局指南:
    references/test-layouts.md
  • Fixture/布局验证器:
    ./scripts/validate-test-layout.sh --root <project>
将测试布局复制到真实项目之前,先使用验证器。它会检查可见静态依赖、元数据和字面结构引用,但不会编译项目,也不能证明 Game Test 可以运行。

Unit Testing (JUnit Jupiter — No Minecraft)

单元测试(JUnit Jupiter——无需 Minecraft)

JUnit Platform task

JUnit Platform 任务

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.
kotlin
tasks.test {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}
对于需要 Loader 设置的 Fabric 代码,使用 Fabric Loader JUnit,不要假设普通 JUnit 会初始化 Minecraft:
groovy
dependencies {
    testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
}

test {
    useJUnitPlatform()
}
当单元测试涉及依赖注册表的 Minecraft 类时,只在测试设置中初始化所需的 bootstrap。当前 Fabric 指南在此场景下使用
SharedConstants.tryDetectVersion()
和
Bootstrap.bootStrap()
。

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

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)

MockBukkit(Paper/Bukkit 插件测试)

The paired dependencies below are a retained 26.2 example. For 26.3, verify a published MockBukkit artifact for that exact Paper line before changing either dependency; a 26.2 mock does not establish 26.3 server behavior.
下面成对的依赖保留自 26.2 示例。对于 26.3,在更改任一依赖之前,先验证是否存在适用于该确切 Paper 版本线的已发布 MockBukkit artifact;26.2 的 mock 无法证明 26.3 的服务器行为。

build.gradle.kts

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

测试兼容 Folia 的调度器抽象

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);
}
MockBukkit 不模拟 Folia 的区域线程运行时。安全的模式是在自定义接口之后封装调度,并对抽象边界进行单元测试。
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

测试 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.
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);
}
这会分发一个合成的死亡事件。对于玩家归因,被测监听器应读取
event.getDamageSource().getCausingEntity()
;实际战斗归因应在真实服务器上单独测试。

Testing item or chunk PDC writes

测试物品或区块 PDC 写入

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

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

NeoForge 1.21.5 及更高版本使用数据驱动的测试环境和测试实例,而不是旧版
@GameTestHolder
方法注册 API。将资源存储在
data/<namespace>/test_environment/
和
data/<namespace>/test_instance/
下。
一个
test_instance
会选择其环境、结构、时序,并指定使用已注册函数还是基于方块的测试。
json
{
  "environment": "minecraft:default",
  "structure": "examplemod:example_structure",
  "max_ticks": 200,
  "setup_ticks": 0,
  "required": true,
  "type": "minecraft:function",
  "function": "examplemod:example_function"
}
通过当前
BuiltInRegistries.TEST_FUNCTION
注册表的
DeferredRegister
注册
Consumer<GameTestHelper>
,然后将该注册器附加到 mod event bus。下面的函数使上述 JSON 引用可用。仅当在代码中而不是数据文件中注册环境和测试实例时,才使用
RegisterGameTestsEvent
。将引用的结构保留在
data/<namespace>/structure/<path>.nbt
中,并显式标记成功。
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();
    }
}
运行
./gradlew runGameTestServer
;服务器退出的结果会给出必需测试的失败数量。此命令适用于真实项目,而不是此 Skill 仓库。

Current Fabric Game Tests (26.x)

当前 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);
    }
}
使用 Fabric Loom 专用的 Game Test source set。在现有的
fabricApi
块中配置它,并将元数据和代码放在
src/gametest
下,与普通单元测试分开。
groovy
fabricApi {
    configureTests {
        createSourceSet = true
        modId = "example-mod-test-${project.name}"
        enableGameTests = true
        enableClientGameTests = true
        eula = true
    }
}
将
fabric.mod.json
放在
src/gametest/resources/
下,并在
fabric-gametest
下注册服务器测试;客户端测试使用
fabric-client-gametest
。使用 Fabric 的
net.fabricmc.fabric.api.gametest.v1.GameTest
实现服务器方法;如果方法运行前需要设置,则使用
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

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.

json
{
  "entrypoints": {
    "fabric-gametest": [
      "com.example.mymod.ExampleGameTest"
    ]
  }
}
确保
fabric-gametest
entrypoint 与具体的 Game Test 类保持同步。Fabric 的服务器 Game Tests 通过
build
运行;客户端测试使用
runClientGameTest
。遵循当前 Fabric 文档,以配置项目特定的 Loom 选项和 headless 客户端 CI。

Legacy NeoForge Game Tests (1.21.3 only)

旧版 NeoForge Game Tests(仅限 1.21.3)

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
.

将基于注解的测试限定在 Minecraft 1.21.3。该类可以通过
@GameTestHolder(MOD_ID)
或
RegisterGameTestsEvent
监听器注册。不要再通过
modEventBus.register(MyGameTests.class)
注册
@GameTestHolder
类。
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();
    }
}
对于
RegisterGameTestsEvent
,在 mod event bus 上注册该类,并在每个
@GameTest
上设置
templateNamespace = MOD_ID
。旧版模板是位于
data/<namespace>/structure/
下的
.nbt
文件;
@PrefixGameTestTemplate(false)
控制是否将类名添加到模板路径。省略
template
时,路径使用小写的方法名;除非禁用此前缀,还会使用小写的简单类名并追加一个点。
template
仅表示路径名;通过
templateNamespace
或
@GameTestHolder
配置其命名空间。

CI

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.

将快速的单元测试/模拟测试与 Loader 的 Game Test 任务分开,并为每个 Minecraft 版本选择相应的 Java 版本:26.x 使用 25,1.21.x 使用 21,Forge 1.20.1 使用 17。运行时相关任务失败时上传测试报告。不要假定其他 Loader 的任务名:Fabric 服务器 Game Tests 使用
build
,而 NeoForge 使用
runGameTestServer
。MockBukkit 无法证明 Folia 线程安全性或真实服务器 bootstrap。

References

参考资料