mcpField
M
minecraft-testing

minecraft-testing

github.com/jahrome907テスト・品質192244 回インストール2 週間前に更新
リポジトリ

minecraft-testing について

Minecraft の MOD やプラグインを開発する人向けの、自動テスト手法とサンプルです。単体、モックサーバー、ゲーム内、統合サーバーのテストを区別し、バージョンやローダーごとの配置方法も案内します。

主な機能

  1. 01JUnit 5 の単体テストガイド
  2. 02MockBukkit によるプラグインテスト
  3. 03Fabric と NeoForge のゲームテスト
  4. 04統合サーバーテストの指針
  5. 05テスト配置の検証スクリプト
  6. 06バージョン別の Java とローダー設定

活用シーン

  1. 01Paper プラグインのイベントやコマンドをテスト
  2. 02MOD のブロックやエンティティのゲーム内テスト
  3. 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 シリアライズいいえ
MockBukkitBukkit/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 のスレッド安全性や 実際のサーバーの初期化を確認できません。


参考資料

コメント