minecraft-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMinecraft Testing Skill
Minecraft 测试 Skill
Testing Strategies Overview
测试策略概览
| Approach | Best For | Requires Game? |
|---|---|---|
| JUnit Jupiter (pure unit tests) | Logic, data structures, NBT serialization | No |
| MockBukkit | Bukkit/Paper plugin events, commands, inventory | No (mocked server) |
| NeoForge GameTests | In-game block/entity/world interaction | Yes (test environment) |
| Fabric GameTests | In-game block/entity/world interaction | Yes (test environment) |
| Integration server | Full plugin/mod lifecycle | Yes (dedicated test server) |
Use 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 序列化 | 否 |
| MockBukkit | Bukkit/Paper 插件事件、命令、物品栏 | 否(模拟服务器) |
| NeoForge GameTests | 游戏内方块/实体/世界交互 | 是(测试环境) |
| Fabric GameTests | 游戏内方块/实体/世界交互 | 是(测试环境) |
| 集成服务器 | 完整的插件/模组生命周期 | 是(专用测试服务器) |
当前的 26.x 项目使用 Java 25。旧版 1.21.x 示例使用 Java 21,Forge 1.20.1 使用 Java 17。不要混用不同版本的源代码结构或 API。
Routing Boundaries
路由边界
- : the task is designing or implementing automated tests (unit, mock, gametest, CI test jobs) for Minecraft projects.
Use when - : the task is implementing gameplay features rather than testing them (
Do not use when,minecraft-modding,minecraft-plugin-dev).minecraft-datapack - : the task is release automation or publishing pipelines (
Do not use when).minecraft-ci-release
- :任务是为 Minecraft 项目设计或实施自动化测试(单元测试、模拟测试、gametest、CI 测试任务)。
Use when - :任务是实现玩法功能而非测试功能(
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
and for that case.
SharedConstants.tryDetectVersion()Bootstrap.bootStrap()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.ktsbuild.gradle.kts
build.gradle.ktskotlin
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 ; test actual
combat attribution separately on a real server.
event.getDamageSource().getCausingEntity()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 method-registration API. Store resources under
and .
A selects its environment, structure, timing, and either a
registered function or a block-based test.
@GameTestHolderdata/<namespace>/test_environment/data/<namespace>/test_instance/test_instancejson
{
"environment": "minecraft:default",
"structure": "examplemod:example_structure",
"max_ticks": 200,
"setup_ticks": 0,
"required": true,
"type": "minecraft:function",
"function": "examplemod:example_function"
}Register the with a for the
current registry, then attach that register to
the mod event bus. The function below makes the JSON reference above usable.
Use only when registering environments and test
instances in code instead of data files. Keep the referenced structure in
and mark success explicitly.
Consumer<GameTestHelper>DeferredRegisterBuiltInRegistries.TEST_FUNCTIONRegisterGameTestsEventdata/<namespace>/structure/<path>.nbtjava
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 ; the server exits with the count of required
failed tests. This command is for a real project, not this skills repository.
./gradlew runGameTestServerNeoForge 1.21.5 及更高版本使用数据驱动的测试环境和测试实例,而不是旧版 方法注册 API。将资源存储在 和 下。
@GameTestHolderdata/<namespace>/test_environment/data/<namespace>/test_instance/一个 会选择其环境、结构、时序,并指定使用已注册函数还是基于方块的测试。
test_instancejson
{
"environment": "minecraft:default",
"structure": "examplemod:example_structure",
"max_ticks": 200,
"setup_ticks": 0,
"required": true,
"type": "minecraft:function",
"function": "examplemod:example_function"
}通过当前 注册表的 注册 ,然后将该注册器附加到 mod event bus。下面的函数使上述 JSON 引用可用。仅当在代码中而不是数据文件中注册环境和测试实例时,才使用 。将引用的结构保留在 中,并显式标记成功。
BuiltInRegistries.TEST_FUNCTIONDeferredRegisterConsumer<GameTestHelper>RegisterGameTestsEventdata/<namespace>/structure/<path>.nbtjava
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();
}
}运行 ;服务器退出的结果会给出必需测试的失败数量。此命令适用于真实项目,而不是此 Skill 仓库。
./gradlew runGameTestServerCurrent Fabric Game Tests (26.x)
当前 Fabric Game Tests(26.x)
Use Fabric Loom's dedicated Game Test source set. Configure it in the existing
block and keep its metadata and code under , separate
from ordinary unit tests.
fabricApisrc/gametestgroovy
fabricApi {
configureTests {
createSourceSet = true
modId = "example-mod-test-${project.name}"
enableGameTests = true
enableClientGameTests = true
eula = true
}
}Place in and register server tests
under ; use for client tests. Implement
server methods with Fabric's and,
when setup is needed before a method runs, .
fabric.mod.jsonsrc/gametest/resources/fabric-gametestfabric-client-gametestnet.fabricmc.fabric.api.gametest.v1.GameTestCustomTestMethodInvokerjava
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。在现有的 块中配置它,并将元数据和代码放在 下,与普通单元测试分开。
fabricApisrc/gametestgroovy
fabricApi {
configureTests {
createSourceSet = true
modId = "example-mod-test-${project.name}"
enableGameTests = true
enableClientGameTests = true
eula = true
}
}将 放在 下,并在 下注册服务器测试;客户端测试使用 。使用 Fabric 的 实现服务器方法;如果方法运行前需要设置,则使用 。
fabric.mod.jsonsrc/gametest/resources/fabric-gametestfabric-client-gametestnet.fabricmc.fabric.api.gametest.v1.GameTestCustomTestMethodInvokerjava
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.jsonsrc/gametest/resources/fabric.mod.json
src/gametest/resources/fabric.mod.jsonjson
{
"entrypoints": {
"fabric-gametest": [
"com.example.mymod.ExampleGameTest"
]
}
}Keep the entrypoint in sync with the concrete Game Test class.
Fabric's server Game Tests run with ; use for client
tests. Follow the current Fabric documentation for project-specific Loom options
and headless client CI.
fabric-gametestbuildrunClientGameTestjson
{
"entrypoints": {
"fabric-gametest": [
"com.example.mymod.ExampleGameTest"
]
}
}确保 entrypoint 与具体的 Game Test 类保持同步。Fabric 的服务器 Game Tests 通过 运行;客户端测试使用 。遵循当前 Fabric 文档,以配置项目特定的 Loom 选项和 headless 客户端 CI。
fabric-gametestbuildrunClientGameTestLegacy 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 or a
listener. Do not register a class
again with .
@GameTestHolder(MOD_ID)RegisterGameTestsEvent@GameTestHoldermodEventBus.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 , register the class on the mod event bus and set
on each . Legacy templates are
files under ;
controls whether the class name is added to the template path. When
is omitted, the path uses the lowercase method name and, unless that prefix is
disabled, the lowercase simple class name followed by a dot. is the
path name only; configure its namespace through or
.
RegisterGameTestsEventtemplateNamespace = MOD_ID@GameTest.nbtdata/<namespace>/structure/@PrefixGameTestTemplate(false)templatetemplatetemplateNamespace@GameTestHolder将基于注解的测试限定在 Minecraft 1.21.3。该类可以通过 或 监听器注册。不要再通过 注册 类。
@GameTestHolder(MOD_ID)RegisterGameTestsEventmodEventBus.register(MyGameTests.class)@GameTestHolderjava
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();
}
}对于 ,在 mod event bus 上注册该类,并在每个 上设置 。旧版模板是位于 下的 文件; 控制是否将类名添加到模板路径。省略 时,路径使用小写的方法名;除非禁用此前缀,还会使用小写的简单类名并追加一个点。 仅表示路径名;通过 或 配置其命名空间。
RegisterGameTestsEvent@GameTesttemplateNamespace = MOD_IDdata/<namespace>/structure/.nbt@PrefixGameTestTemplate(false)templatetemplatetemplateNamespace@GameTestHolderCI
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 , while NeoForge
uses . MockBukkit does not prove Folia thread safety or real
server bootstrap.
buildrunGameTestServer将快速的单元测试/模拟测试与 Loader 的 Game Test 任务分开,并为每个 Minecraft 版本选择相应的 Java 版本:26.x 使用 25,1.21.x 使用 21,Forge 1.20.1 使用 17。运行时相关任务失败时上传测试报告。不要假定其他 Loader 的任务名:Fabric 服务器 Game Tests 使用 ,而 NeoForge 使用 。MockBukkit 无法证明 Folia 线程安全性或真实服务器 bootstrap。
buildrunGameTestServerReferences
参考资料
- 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 Jupiter user guide: https://junit.org/junit5/docs/current/user-guide/
- MockBukkit GitHub:https://github.com/MockBukkit/MockBukkit
- MockBukkit 文档:https://docs.mockbukkit.org/
- Fabric 自动化测试: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 Jupiter 用户指南:https://junit.org/junit5/docs/current/user-guide/