minecraft-testing について
Minecraft の MOD やプラグインを開発する人向けの、自動テスト手法とサンプルです。単体、モックサーバー、ゲーム内、統合サーバーのテストを区別し、バージョンやローダーごとの配置方法も案内します。
主な機能
- 01JUnit 5 の単体テストガイド
- 02MockBukkit によるプラグインテスト
- 03Fabric と NeoForge のゲームテスト
- 04統合サーバーテストの指針
- 05テスト配置の検証スクリプト
- 06バージョン別の Java とローダー設定
活用シーン
- 01Paper プラグインのイベントやコマンドをテスト
- 02MOD のブロックやエンティティのゲーム内テスト
- 03CI で高速テストとゲームテストを分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jahrome907/ minecraft-agent-skills --skill minecraft-testingインストール後、Claude Code で/minecraft-testingを入力して読み込まれたか確認します。
同梱ファイルreferences/test-layouts.mdscripts/validate-test-layout.sh
詳細
Minecraft テストスキル
テスト戦略の概要
| アプローチ | 最適な用途 | ゲームが必要か |
|---|---|---|
| JUnit 5(純粋な単体テスト) | ロジック、データ構造、NBT シリアライズ | いいえ |
| MockBukkit | Bukkit/Paper プラグインのイベント、コマンド、インベントリ | いいえ(サーバーをモック) |
| NeoForge GameTests | ゲーム内のブロック/エンティティ/ワールドとのインタラクション | はい(テスト環境) |
| Fabric GameTests | ゲーム内のブロック/エンティティ/ワールドとのインタラクション | はい(テスト環境) |
| インテグレーションサーバー | プラグイン/MOD のライフサイクル全体 | はい(専用のテストサーバー) |
現在の 26.x プロジェクトでは Java 25 を使用してください。1.21.x のレガシーな例では Java 21、Forge 1.20.1 では Java 17 を使用してください。バージョンをまたいでソースのレイアウトや API を混在させないでください。
振り分けの境界
Use when:Minecraft プロジェクトの自動テスト(単体テスト、モック、ゲームテスト、CI テストジョブ)を設計または実装するタスクです。Do not use when:テストではなく、ゲームプレイ機能を実装するタスクです(minecraft-modding、minecraft-plugin-dev、minecraft-datapack)。Do not use when:リリースの自動化や公開パイプラインに関するタスクです(minecraft-ci-release)。
同梱のリファレンスとヘルパー
- レイアウトガイド:
references/test-layouts.md - フィクスチャ/レイアウト検証ツール:
./scripts/validate-test-layout.sh --root <project>
実際のプロジェクトにテストレイアウトをコピーする前に、検証ツールを使用してください。目に見える静的依存関係、メタデータ、リテラルな構造参照をチェックします。プロジェクトのコンパイルや、ゲームテストの実行を保証するものではありません。
単体テスト(JUnit 5 — Minecraft 不要)
JUnit Platform タスク
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
ローダーのセットアップが必要な Fabric コードでは、通常の JUnit が Minecraft を初期化すると想定せず、Fabric Loader JUnit を使用してください。
dependencies {
testImplementation "net.fabricmc:fabric-loader-junit:${project.loader_version}"
}
test {
useJUnitPlatform()
}
単体テストでレジストリに依存する Minecraft クラスを使用する場合は、テストのセットアップで必要なブートストラップのみを初期化してください。現在の Fabric ガイドでは、このケースに SharedConstants.tryDetectVersion() と Bootstrap.bootStrap() を使用しています。
純粋な単体テストの例
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CooldownManagerTest {
@Test
void cooldown_tracksPlayersIndependently() {
var manager = new CooldownManager(500L); // 500ms cooldown
manager.startCooldown("steve");
assertTrue(manager.isOnCooldown("steve"));
assertFalse(manager.isOnCooldown("notExisting"));
}
@Test
void cooldown_throwsIllegalArgument_onNegativeDuration() {
assertThrows(IllegalArgumentException.class,
() -> new CooldownManager(-1L));
}
}
MockBukkit(Paper/Bukkit プラグインのテスト)
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()
}
セットアップ/ティアダウンのパターン
import org.mockbukkit.mockbukkit.MockBukkit;
import org.mockbukkit.mockbukkit.ServerMock;
import org.mockbukkit.mockbukkit.entity.PlayerMock;
import org.junit.jupiter.api.*;
class MyPluginTest {
private static ServerMock server;
private static MyPlugin plugin;
@BeforeAll
static void setUp() {
// Start mock Bukkit server and load your plugin
server = MockBukkit.mock();
plugin = MockBukkit.load(MyPlugin.class);
}
@AfterAll
static void tearDown() {
MockBukkit.unmock();
}
}
イベントのテスト
@Test
void playerJoin_getsWelcomeMessage() {
PlayerMock player = server.addPlayer("Steve");
player.simulateJoin(); // fires PlayerJoinEvent
// Assert the player received the expected message component
player.assertSaid("Welcome, Steve!");
// Or for Adventure components:
assertTrue(player.nextMessage().contains("Welcome"));
}
@Test
void onBlockBreak_cancelledForNonOp() {
PlayerMock player = server.addPlayer();
player.setOp(false);
Block block = player.getWorld().getBlockAt(0, 64, 0);
block.setType(Material.STONE);
BlockBreakEvent event = new BlockBreakEvent(block, player);
server.getPluginManager().callEvent(event);
assertTrue(event.isCancelled(), "Non-op should not be able to break blocks");
}
コマンドのテスト
@Test
void mypluginInfo_returnsVersion() {
PlayerMock player = server.addPlayer("Admin");
player.setOp(true);
boolean result = server.dispatchCommand(player, "myplugin info");
assertTrue(result);
player.assertSaid("Version: " + plugin.getDescription().getVersion());
}
@Test
void mypluginReload_requiresOp() {
PlayerMock player = server.addPlayer("NonOp");
player.setOp(false);
server.dispatchCommand(player, "myplugin reload");
player.assertSaid("No permission.");
}
インベントリ/アイテムのテスト
@Test
void giveKitCommand_givesPlayerItems() {
PlayerMock player = server.addPlayer();
server.dispatchCommand(player, "kit starter");
// Check inventory
assertTrue(player.getInventory().contains(Material.STONE_SWORD));
assertTrue(player.getInventory().contains(Material.BREAD, 16));
}
スケジューラータスクのテスト
@Test
void repeatingTask_firesAfterDelay() {
PlayerMock player = server.addPlayer();
// Execute 40 ticks worth of scheduled tasks
server.getScheduler().performTicks(40L);
// Assert expected side effect happened
assertEquals(2, plugin.getTaskCount());
}
Folia 対応スケジューラー抽象化のテスト
MockBukkit は Folia のリージョンスレッド型ランタイムをエミュレートしません。安全な方法は、スケジューリングを独自のインターフェイスの背後にラップし、抽象化の境界を単体テストすることです。
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);
}
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);
}
これは合成された死亡イベントをディスパッチします。プレイヤーの帰属を判定する場合、テスト対象のリスナーは event.getDamageSource().getCausingEntity() を読み取る必要があります。実際の戦闘における帰属判定は、実サーバーで別途テストしてください。
アイテムまたはチャンクへの PDC 書き込みのテスト
@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);
}
現行の NeoForge Game Tests(26.x)
NeoForge 1.21.5 以降では、旧式の @GameTestHolder メソッド登録 API ではなく、データ駆動型のテスト環境とテストインスタンスを使用します。リソースは data/<namespace>/test_environment/ と data/<namespace>/test_instance/ に格納してください。test_instance は、環境、構造、タイミング、および登録済み関数かブロックベースのテストのいずれかを指定します。
{
"environment": "minecraft:default",
"structure": "examplemod:example_structure",
"max_ticks": 200,
"setup_ticks": 0,
"required": true,
"type": "minecraft:function",
"function": "examplemod:example_function"
}
Consumer<GameTestHelper> を現在の BuiltInRegistries.TEST_FUNCTION レジストリ用の DeferredRegister に登録し、そのレジスターを MOD のイベントバスに追加してください。以下の関数を使うと、上記の JSON から参照できます。データファイルの代わりにコードで環境とテストインスタンスを登録する場合に限り、RegisterGameTestsEvent を使用してください。参照される構造は data/<namespace>/structure/<path>.nbt に格納し、成功を明示的に示してください。
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 を実行してください。サーバーは必須テストの失敗数を終了コードとして返します。このコマンドは実際のプロジェクト用であり、このスキルのリポジトリ用ではありません。
現行の Fabric ゲームテスト(26.x)
Fabric Loom 専用のゲームテストソースセットを使用します。既存の
fabricApi ブロックで設定し、メタデータとコードは通常のユニットテストと
分けて src/gametest の下に配置します。
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 を使用します。
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"
]
}
}
fabric-gametest のエントリーポイントは、実際のゲームテストクラスと一致させてください。
Fabric のサーバーゲームテストは build で実行します。クライアントテストには
runClientGameTest を使用します。プロジェクト固有の Loom オプションと
ヘッドレスクライアント CI については、最新の Fabric ドキュメントに従ってください。
レガシー NeoForge ゲームテスト(1.21.3 のみ)
アノテーションベースのテストは Minecraft 1.21.3 専用に分離してください。クラスは
@GameTestHolder(MOD_ID) または RegisterGameTestsEvent リスナーのいずれかで
登録できます。@GameTestHolder クラスを
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();
}
}
RegisterGameTestsEvent では、クラスを mod イベントバスに登録し、各
@GameTest に templateNamespace = MOD_ID を設定します。レガシーのテンプレートは、
data/<namespace>/structure/ の下にある .nbt ファイルです。
@PrefixGameTestTemplate(false) は、クラス名をテンプレートのパスに追加するかどうかを
制御します。template を省略すると、パスにはメソッド名を小文字にしたものが使用され、
プレフィックスが無効化されていない場合は、その後に単純クラス名を小文字にしたものと
ピリオドが続きます。template はパス名のみを指定します。名前空間は
templateNamespace または @GameTestHolder で設定してください。
CI
高速なユニットテスト/モックテストは、ローダーのゲームテストタスクと分けてください。
Minecraft のバージョンごとに Java のバージョンを選択します。26.x では 25、1.21.x では
21、Forge 1.20.1 では 17 を使用します。実行時の動作を対象とするジョブが失敗した場合は、
テストレポートをアップロードしてください。他のローダーのタスク名をそのまま使えると
想定しないでください。Fabric のサーバーゲームテストは build で実行されますが、
NeoForge では runGameTestServer を使用します。MockBukkit では、Folia のスレッド安全性や
実際のサーバーの初期化を確認できません。
参考資料
- MockBukkit GitHub:https://github.com/MockBukkit/MockBukkit
- MockBukkit ドキュメント:https://docs.mockbukkit.org/
- Fabric 自動テスト:https://docs.fabricmc.net/develop/automatic-testing
- NeoForge 26.x ゲームテスト:https://docs.neoforged.net/docs/misc/gametest/
- NeoForge 1.21.3 ゲームテスト:https://docs.neoforged.net/docs/1.21.3/misc/gametest/
- JUnit 5 ユーザーガイド:https://junit.org/junit5/docs/current/user-guide/