Minecraft Game Test 使い方メモ

# Game Testとは
Game TestはMinecraftにおけるテスト用フレームワークです。このフレームワークはクライアントとサーバーの両方で動作します。クライアントでの実行では実際にテストが進んでいる様子を見られます。サーバーはGUIなしで動作するためCIの中に組み込んで実行できます。

JavaにはJUnitといった様々なテストフレームワークが存在します。これらのユニットテストのターゲットはMinecraftには狭く，十分なテストをするためには不向きです。ユニットテストでは関数がどのように働くかをテストできます。しかし実際のワールドでの動作を検証するためにはほかの仕組みが必要です。

この文章ではMinecraft Forgeの環境を使用します。バニラではユーザー側でGame Testを実行する手段はありません([バニラ](#バニラ)を見てください)。FabricはForgeと少し異なる仕組みでGame Testを実行するため，この記事の内容の範囲外です。

# 環境
Game Testの実行にはこの環境を使用しています。
```
> ./gradlew --version

------------------------------------------------------------
Gradle 7.5.1
------------------------------------------------------------

Build time:   2022-08-05 21:17:56 UTC
Revision:     d1daa0cbf1a0103000b71484e1dbfe096e095918

Kotlin:       1.6.21
Groovy:       3.0.10
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.4.1 (Eclipse Adoptium 17.0.4.1+1)
OS:           Windows 10 10.0 amd64
```

# Game Testを実行する

## バニラ

Forgeのパッチを見る限り，`SharedConstants.IS_RUNNING_IN_IDE`が`true`となっている場合にGame Testが有効になります。しかしこの変数に直接アクセスしているコードは存在していないようです(リフレクションのアクセスについてはわかりません)。そのためバニラの環境ではGame Testは実行できないものと思われます。Modのフレームワークを問わず，Minecraft Launcherから実行できるMinecraftではGame Testは実行できません。

## Forge

### Forge環境のセットアップ

1. [公式の配布サイト](https://files.minecraftforge.net/net/minecraftforge/forge/)から最新のMDKを入手してください.
2. 解凍して好きな場所に置いてください

### ヘッドレスのGame Testの設定を追加する

最近MDKを入手していればこのステップの内容はすでに入っているため，飛ばせます。

また，Clientでのみテストを実行する場合も不要です。Forgeの環境では開発用のClientでテストコマンドが使用できます。

`build.gradle`の中の，`minecraft.runs`に実行設定が書かれています。
以下の行を追加してGame Test Serverの構成を追加します。
```groovy
// This code is from official MDK, licensed under the LGPL 2.1. 
// This run config launches GameTestServer and runs all registered gametests, then exits.
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
    workingDirectory project.file('run')

    property 'forge.logging.markers', 'REGISTRIES'

    property 'forge.logging.console.level', 'debug'

    property 'forge.enabledGameTestNamespaces', 'YOUR_MOD_ID'

    forceExit false

    mods {
        "YOUR_MOD_ID" {
            source sourceSets.main
        }
    }
}
```
`workingDirectory`を変更すると通常の実行環境とフォルダを分けられます。modsの中の`source`を`sourceSets.test`にするとtestフォルダの中のソースコードを読み込んで実行できます。

そうしたら`./gradlew runGameTestServer`を実行しましょう. 1つもテストがないのでテストは成功します。

:::note warn
Clientの実行設定の中にも`property 'forge.enabledGameTestNamespaces', 'YOUR_MOD_ID'`の記述は必要です。
もし`/test runall`を実行しても何もテストが行われない場合や，`/test run <test name>`で補完が出こてない場合はこの設定が追加されているか確認して下さい。
:::

### Game Testを追加する

必要なもの

- [コード](#コード)
- [`snbt`ファイル](#Structure)

#### コード
ここではForgeのMDKをそのまま使用することを想定します。mod idやパッケージはMDKのデフォルトの値を使用しています。例を参考にする際は環境ごとに読み替えてください

ForgeのアノテーションでGame Testを登録する例です。
```java
package com.example.examplemod.gametest;

import com.example.examplemod.ExampleMod;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.gametest.GameTestHolder;

@GameTestHolder(value = ExampleMod.MODID)
public final class RegisterViaAnnotation {
    @GameTest
    public void placeDirt(GameTestHelper helper) {
        var pos = BlockPos.ZERO.above();
        helper.setBlock(pos, Blocks.DIRT);
        helper.succeedIf(() -> helper.getBlockState(pos).is(Blocks.DIRT));
    }
}
```
このテストでは基準の位置(テストごとに1つ用意される，ストラクチャーブロックの位置です)の1つ上に土ブロックを置き，土ブロックがその位置に存在するか確かめます。

:::note warn
コードだけではStructureが見つかりませんと言われて実行できません
:::

#### Structure

上の例ではブロックを手動で設置しました。ブロックを大量に置く複雑なテストでは，ブロックを手動で置いているとミスも起きやすく何より面倒です。そのためMinecraftではテストの構造を`snbt`ファイルから読み込むようになっています。`snbt`ファイルは(たとえ手動でブロックを置くとしても)必須です。
`snbt`ファイルはセーブデータが保存されているフォルダの`gamestructure`に置かれる必要があります。`mods`や`config`と同じ階層です。

**`snbt`ファイルの生成方法については[testコマンドの詳細](https://qiita.com/Kotori316/items/bf4e417feb5e964a46f3)をみてください。**

ストラクチャーブロックから生成される`nbt`ブロックとは違い，`snbt`ファイルはJsoncに似た人の読み書きできる形式です。Minecraftの中で実際にブロックを置き，ストラクチャーブロックやtestコマンドを使って出力します。必要となる`snbt`ファイルの名前は`<class name>.<method name>.snbt`です。上の例では(class name=`RegisterViaAnnotation`, method name=`placeDirt`)ファイル名が`registerviaannotation.placedirt.snbt`となります。

サイズが(2, 2, 2)の`snbt`ファイルの例です。
```jsonc
{
    DataVersion: 3120,
    size: [2, 2, 2],
    data: [
        {pos: [0, 0, 0], state: "minecraft:polished_andesite"},
        {pos: [0, 0, 1], state: "minecraft:polished_andesite"},
        {pos: [1, 0, 0], state: "minecraft:polished_andesite"},
        {pos: [1, 0, 1], state: "minecraft:polished_andesite"},
        {pos: [0, 1, 0], state: "minecraft:air"},
        {pos: [0, 1, 1], state: "minecraft:air"},
        {pos: [1, 1, 0], state: "minecraft:air"},
        {pos: [1, 1, 1], state: "minecraft:air"}
    ],
    entities: [],
    palette: [
        "minecraft:polished_andesite",
        "minecraft:air"
    ]
}
```

### Run

コードと`snbt`ファイルを用意するとGame TestをClientとServerで実行できます。

#### Clientで実行する

1. `./gradlew runClient`を実行するかIDEからMinecraftを起動してください
2. 適当にワールドを作成して入ってください。デフォルトの設定のスーパーフラットが見やすいです。
3. チャット欄から`/test runall`を実行するとテストされます。結果はチャットやビーコンで知らせてくれます。

#### Serverで実行する

`./gradlew runGameTest`を実行してください。すべてのテストが自動で行われます。

結果はログにこのように表示されます。
```
========= 2 GAME TESTS COMPLETE ======================
All 2 required tests passed :)
====================================================
```

`gradlew`がデーモンのログを残して失敗するがテストは成功しているように見える場合は，`forceExit false`を上の例と同じように追加すると改善されます。

### まとめ

必要なもの
- `build.gradle`中の設定
  -  CIで動かすなどヘッドレスでテストしたい場合
- Javaのテストコード
    - クラスには`@GameTestHolder(ExampleMod.MODID)`のアノテーション
    - メソッドには`@GameTest`のアノテーション
    - アノテーションの詳細は後に記載
- `snbt`ファイル
    - `run/gameteststructures`に設置
        - `run`は`runClient`や`runGameTestServer`の作業フォルダ
    - `snbt`ファイルの作成法は[testコマンドの詳細](https://qiita.com/Kotori316/items/bf4e417feb5e964a46f3)に記載

フォルダ構造
```
.
├── build.gradle
├── run (runClientやrunGameTestServerのworking directory)
│   └── gameteststructures
│       └── registerviaannotation.placedirt.snbt
└── src (ソースコード Java以下は任意です)
    └── main
        └── java
            └── com
                └── example
                    └── examplemod
                        └── gametest
                            └── RegisterViaAnnotation.java
```

# テストの書き方

## テストの追加方法

- `@GameTestHolder`のアノテーション
  - Class
- `RegisterGameTestsEvent`のイベント
  - Class
  - Method
- `GameTestRegistry#register`(deprecated)の呼び出し
  - Class
  - Method
  - 上のイベントの呼び出しで代替できる
    - Forgeはイベントかアノテーションを推奨している

上の2つの方法で対象となるクラスやメソッドを追加します。そのうえで個々のメソッドにつけられた以下のアノテーションでテストが追加されます。
`RegisterGameTestsEvent`でMethodを指定しても以下のアノテーションがついていないと登録されないので注意してください。

- `@GameTest`でメソッドを追加
- `@GameTestGenerator`で動的な追加

### `@GameTest`での追加

`@GameTest`はメソッドにつけるアノテーションです。

- staticでもinstanceメソッドでもOK
  - instanceメソッドの場合，インスタンスがテスト実行前に作られる
     - フィールドにデータを書き込んでも別のテストからは見えないので注意
  - その場合リフレクションで引数なしのコンストラクタが呼び出される
      - 明示的にコンストラクタを作成していなければ暗黙的に引数なしのコンストラクタが作られている(Javaの仕様)
- 可視性はpublic
  - 最終的にリフレクションで呼ばれる
  - publicでない場合は`IllegalAccessException`で落ちる
- 引数は`GameTestHelper`を1つ
  - リフレクションなので過不足ある場合は実行時に落ちる
- 返り値の型は`void`でOK
  - 返り値は破棄されるので実際はなんでもいい
- 成功時には`GameTestHelper#succeed`を呼ぶ
  - これがテスト終了の合図になる
  - 呼ばないと`timeoutTicks`内に終了しなかったとしてFailedになる

`@GameTest`のアノテーションにはテストの実行に関する様々なパラメータを指定できます。詳細は[ここ](#gametestのパラメータ)を見てください。

### `@GameTestGenerator`での追加

`@GameTestGenerator`はメソッドにつけるアノテーションです。

- staticでもinstanceメソッドでもOK
  - instanceメソッドの場合，インスタンスがテスト実行前に作られる
  - その場合リフレクションで引数なしのコンストラクタが呼び出される
- 可視性はpublic
- 引数はなし
- 返り値の型は`Collection<TestFunction>`
  - `List`などの`Collection`の子クラスならOK

`TestFunction`ではテストのパラメータを指定し，実行する内容を`Consumer<GameTestHelper>`として渡します。渡すパラメータの詳細は[ここ](#testfunctionのパラメータ)を見てください。

### 例

```java
@GameTestHolder(value = ExampleMod.MODID)
public class RegisterViaAnnotation {
    @GameTest
    public void instance_test1(GameTestHelper helper) {
        helper.succeed();
    }

    @GameTest
    public static void static_test2(GameTestHelper helper) {
        helper.succeed();
    }

    @GameTestGenerator
    public Collection<TestFunction> generator2(){
        return List.of(new TestFunction(
            "defaultBatch", /* batch name */
            "generated_test", /* test name */
            "%s:test.structure".formatted(ExampleMod.MODID), /* structure name */
            100, /* max ticks */
            0L, /* setup ticks */
            true, /* required */
            g -> {
                g.assertBlock(new BlockPos(0, 1, 0), Blocks.AIR::equals, "");
                g.succeed();
            }
        ));
    }
}
```

## `@GameTestHolder`のパラメータ

* value: String
  * 対象となるnamespaceを指定する
  * `build.gradle`中の`property 'forge.enabledGameTestNamespaces'`の値と一致させる
    * それか`forge.enabledGameTestNamespaces`の値を空にする
    * 全modがテストの対象になる
  * 省略すると`minecraft`が使われる

## `@GameTest`のパラメータ

* attempts: int
  * default: `1`
  * 試行回数
    * この試行回数の中で`requiredSuccesses`の回数以上成功していればテストはPassed
    * ただし最初の試行で成功しないとCIが失敗する(Exit Codeが1になる)
  * この試行回数を取得する方法は用意されていない
* batch: String
  * default: `"defaultBatch"`
  * テストの属するバッチの名前
      * バッチは実行する単位のようなもの
      * 同一のバッチは並列に実行される
* required: boolean
  * default: `true`
  * `false`だとテストの失敗がskipまたはignoreとして記録される
    * テストに失敗してもCIなどが通るようになる
* requiredSuccesses: int
  * default: `1`
  * `attempts`で試行回数をしているしている場合の必要な成功回数
* rotationSteps: int
  * default: `0`
    * 0-3の値をとる
    * それ以外では実行時に落ちる
  * テストの構造体の回転を表すパラメータ
  * 引数と角度の関係は[testコマンドの説明](https://qiita.com/Kotori316/items/bf4e417feb5e964a46f3#run-test-name-rotation-steps)を参照
* setupTicks: long
  * default: `0L`
  * 構造体が設置されてからテストが実行されるまでの間隔
* template: String
  * default: `""`
  * テストの構造体の名前
  * 詳細は[ここ](#gametestでの構造体の名前)
* templateNamespace: String
  * default: `""`
  * テストのnamespaceを指定する
  * `@GameTestHolder`のnamespaceを上書きする場合に使う
  * `forge.enabledGameTestNamespaces`に入っていないnamespaceを指定するとテストが読み込まれなくなる
* timeoutTicks: int
  * default: `100`
  * テストの実行時間
    * この時間までに終了しないとFailedになる

## `TestFunction`のパラメータ

引数が最も多いコンストラクタの引数の順番通りに記載します。
公式のMappingでは引数の名前は与えられていないので対応するフィールドの名前で紹介しています。
- batch name: String
    - テストの属するバッチの名前
- test name: String
    - テストの名前
- structure name: String
    - テストに使用する構造体の名前
    - Stringではあるが，ResouceLocationとして有効な名前である必要がある
- rotation: Rotation
    - `net.minecraft.world.level.block.Rotation`のインスタンス
    - テストの構造体の回転方向
    - 引数の少ないコンストラクタでのデフォルトは`Rotation.NONE`
- max ticks: int
    - テストの最大実行時間
    - 単位はtick
    - テストがこの時間を超えるとFailedになる
- setup ticks: long
    - 構造体が設置されてからテストが実行されるまでの時間
    - 単位はtick
- required: boolean
    - テストが失敗した際にもCIを通すか
    - requiredが`true`ならテストが失敗すると全体としても失敗になる
    - `false`だとテストが失敗しても全体は失敗扱いにならない
- required successes: int
    - 複数回実行した際に何回の成功が必要になるかの数
    - 次のmax attemptsと組み合わせて使う
- max attempts: int
    - テストを実行する回数
- function: Consumer\<GameTestHelper\>
    - テストの本体

## 構造体の名前について

構造体の名前は`@GameTest`の`template`などや`TestFunction`の`structureName`で指定できます。

### `@GameTest`での構造体の名前

構造体の名前を決定するのにかかわる要素は以下のものです。

- `@GameTest`の`template`
- `@GameTestHolder`の値
  - または`@GameTest`の`templateNamespace`
- `@PrefixGameTestTemplate`の値

構造体の名前はResourceLocationの形式です(`namespace:path`)。

1. namespaceは`@GameTestHolder`で指定した名前が入ります。`@GameTest`の`templateNamespace`がある場合はこちらが優先されます。どちらもない場合は"minecraft"となります。
2. pathは"\<ClassName\>.\<template\>"の名前が入ります。
   - `@GameTest`で`template`を指定しないか""とした場合は代わりにメソッド名を小文字にしたものが\<template\>になります。
   - `@PrefixGameTestTemplate`のアノテーションで`false`を指定した場合はpathが"\<template\>"のみとなります。
     - このアノテーションはクラスかメソッドにつけられます。クラスにつけた場合はそのクラスのメソッドすべてが対象になります。
     - アノテーションをつけなかった場合は`true`とみなされ"\<ClassName\>"が付加されます。
     - クラス名に関係ないファイル名が使えるため他クラスとの構造体ファイルの共有が可能になります。
3. 上のnamespaceとpathから`namespace:path`として決定されます

#### 例

<table>
<tr> <td>method definition</td> <td>structure name</td> </tr>
<tr>
  <td>
  
  ```java
  @GameTestHolder("m1")
  class C1 { 
    @GameTest() 
    public void testNoArguments(GameTestHelper helper) {}
  }
  ```
  </td>
  <td>m1:c1.testnoarguments</td>
</tr>
<tr>
  <td>
  
  ```java
  @GameTestHolder("m1")
  class C1 { 
    @GameTest(template="custom") 
    public void testCustomTemplate(GameTestHelper helper) {}
  }
  ```
  </td>
  <td>m1:c1.custom</td>
</tr>
<tr>
  <td>
  
  ```java
  @GameTestHolder("m1")
  class C1 { 
    @GameTest(template="custom", templateNamespace="mod2") 
    public void testCustomNamespace(GameTestHelper helper) {}
  }
  ```
  </td>
  <td>mod2:c1.custom</td>
</tr>
<tr>
  <td>
  
  ```java
  @GameTestHolder("m1")
  @PrefixGameTestTemplate(value = false)
  class C1 { 
    @GameTest(template="custom") 
    public void testNoPrefixAndCustomName(GameTestHelper helper) {}
  }
  ```
  </td>
  <td>m1:custom</td>
</tr>
</table>

### `TestFunction`での構造体の名前

`structureName`として渡した名前がそのまま使われます。

### 使用される構造体の決定

まず，MinecraftやModで読み込まれる構造体(StructureTemplate)から該当する構造体があるか検索されます。
この構造体は`data/namespace/structures`以下に保存された`nbt`ファイルです。
Minecraftで登録されているのは村やネザー遺跡といった自然生成される構造物に関する構造体です。

もし該当する構造体がなかった場合はテスト用の構造体が検索されます。
ここでの検索には構造体の名前のPathのみが使用されます。
例えば，`m1:c1.custom`といった構造体では`gameteststructures/c1.custom.snbt`のファイルから構造体が読み込まれます。
この`gameteststructures`は`mods`や`config`といったディレクトリと同じ階層に置きます。
ファイルが存在しない場合は`RuntimeException`が投げられます。

## バッチ

バッチはテストを実行するかたまりです。
同じバッチに属するテストは並列に実行されます。
天気や時間を設定したうえでテストする場合など，ほかの条件と混ぜてはいけない場合にバッチを分けます。
また，次に説明する前処理や後処理もバッチ単位で設定できます。

テストが登録された後，内部的に100個以下のバッチに再分割されます。
そのときバッチには連番が付与されます。
テストのログに`defaultBatch1`とあれば`defaultBatch`の中の1番目ということです。
同一バッチ内にテストが101個以上ある場合は`defaultBatch2`といったように次の番号のバッチが作成されます。

### 前処理と後処理

バッチごとに1回ずつ前処理と後処理を入れることができます。

前処理には`@BeforeBatch(batch = "batchName")`のアノテーションをつけます。`@GameTest`と同様staticでもインスタンスメソッドでも使えます。メソッドの引数は`ServerLevel`が1つです。
同じバッチに対して複数の`@BeforeBatch`が存在する場合には登録の時点でエラーになります。
後処理には`@AfterBatch(batch = "batchName")`のアノテーションをつけます。

例
```java
@GameTestHolder(value = ExampleMod.MODID)  
public final class BeforeAfterBatch {  
    private static final Logger LOGGER = LogManager.getLogger();  
  
    @BeforeBatch(batch = "defaultBatch")  
    public static void before(ServerLevel level) {  
        LOGGER.info("BeforeBatch is called with level {}", level.getSeed());  
    }  
  
    @AfterBatch(batch = "defaultBatch")  
    public static void after(ServerLevel level) {  
        LOGGER.info("AfterBatch is called with level {}", level.getSeed());  
    }  
}
```

## `GameTestHelper`の機能

`GameTestHelper`にはテストワールドに関する機能とアサーションの関数が含まれています。
ワールドの機能としてはブロックの設置，エンティティのスポーン，ボタンとレバーの操作があります。
また`getLevel`で`ServerLevel`のインスタンスを取得し，直接操作することも可能です。

:::note warn
GameTestにおける座標はストラクチャーブロックを(0, 0, 0)とする相対座標(relative position)で表されます。
`GameTestHelper`の関数は内部で相対座標から絶対座標に変換されています。
`ServerLevel`のインスタンスを直接取得したり`BlockEntity`を取得して座標を渡す場合にはこちらで座標の変換処理を忘れずに入れる必要があります。
その変換には`absolutePos(BlockPos)`と`relativePos(BlockPos)`を使います。
:::

アサーションの関数にはブロックやモブの存在を確認するものやコンテナに関するものがあります。
JUnitによくある`assertTrue`などは存在しないため自分でifの分岐を書く必要があります。

アサーションに失敗した際には`GameTestAssertException`やその子クラスの`GameTestAssertPosException`が投げられます。
`RuntimeException`の子クラスであればテスト失敗として扱われます。その他（例えば`AssertionError`）だとMinecraftのクラッシュとなり，その後のテストが実行されません。

その他の機能については省略します。ソースコードとメソッド名を見れば何ができるかはわかります。
モブの移動など数tickかかる動作を検証したい場合には`GameTestSequence`が使えます。
`GameTestHelper#startSequence`で開始できます。
メソッドチェーンでtickごとの処理を書いたり数tick待つといったことができます。

# 参考
- https://www.youtube.com/watch?v=vXaWOJTCYNg
    - In this video, the concept of Game Test is shown.
- https://github.com/MinecraftForge/MinecraftForge 
    - The GitHub code of [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/).