Minecraft游戏测试使用说明笔记
游戏测试是什么?
游戏测试是Minecraft中用于测试的框架。此框架可在客户端和服务器上同时运行。在客户端执行时可以实际观察到测试的进行情况。由于服务器是无GUI运行的,因此可以将其嵌入到CI中进行执行。
在Java中,存在许多测试框架,比如JUnit。这些单元测试的目标对于Minecraft来说太局限了,无法进行充分的测试。单元测试可以测试函数的工作方式,但是要验证在实际世界中的运行情况,则需要其他机制。
この文章ではMinecraft Forgeの環境を使用します。バニラではユーザー側でGame Testを実行する手段はありません(バニラを見てください)。FabricはForgeと少し異なる仕組みで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
执行游戏测试
香草
Forgeのパッチを見る限り,SharedConstants.IS_RUNNING_IN_IDEがtrueとなっている場合にGame Testが有効になります。しかしこの変数に直接アクセスしているコードは存在していないようです(リフレクションのアクセスについてはわかりません)。そのためバニラの環境ではGame Testは実行できないものと思われます。Modのフレームワークを問わず,Minecraft Launcherから実行できるMinecraftではGame Testは実行できません。
Forge
设置Forge环境
-
- 请从公式分发网站下载最新版本的MDK。
- 将其解压并放置在您喜欢的位置。
添加无头游戏测试的设置
最近如果已经获得MDK,那么这个步骤的内容已经包含在里面了,可以跳过。
此外,如果只在客户端进行测试,也不需要。在Forge环境中,可以在开发用客户端中使用测试命令。
在build.gradle文件中,我们可以找到minecraft.runs中的运行配置。
通过添加以下行,我们可以添加Game Test Server的配置。
// 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。由于没有进行任何测试,所以测试将会成功。
Clientの実行設定の中にもproperty ‘forge.enabledGameTestNamespaces’, ‘YOUR_MOD_ID’の記述は必要です。
もし/test runallを実行しても何もテストが行われない場合や,/test run で補完が出こてない場合はこの設定が追加されているか確認して下さい。
添加游戏测试
必需品 (bì xū
-
- コード
- snbtファイル
代码 (Mandarin Chinese)
在这里,假设我们直接使用Forge的MDK。mod id和包名使用了MDK的默认值。请根据实际环境参考示例中的内容。
这是使用Forge注释注册Game Test的示例。
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つ上に土ブロックを置き,土ブロックがその位置に存在するか確かめます。
コードだけではStructureが見つかりませんと言われて実行できません
结构
上の例ではブロックを手動で設置しました。ブロックを大量に置く複雑なテストでは,ブロックを手動で置いているとミスも起きやすく何より面倒です。そのためMinecraftではテストの構造をsnbtファイルから読み込むようになっています。snbtファイルは(たとえ手動でブロックを置くとしても)必須です。
snbtファイルはセーブデータが保存されているフォルダのgamestructureに置かれる必要があります。modsやconfigと同じ階層です。
请查看有关test命令的详细信息,以了解如何生成snbt文件。
与从Structure Block生成的NBT块不同,SNBT文件是一种类似于Jsonc的可读写格式。在Minecraft中实际放置方块,使用Structure Block或test命令进行输出。所需的SNBT文件名为..snbt。在上述示例中,文件名为registerviaannotation.placedirt.snbt。
サイズが(2, 2, 2)のsnbtファイルの例です。
{
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"
]
}
奔跑
只需准备好代码和SNBT文件,就可以在客户端和服务器中执行游戏测试。
在客户端上执行
-
- 请执行./gradlew runClient或从IDE启动Minecraft
-
- 创建一个适当的世界并进入。建议使用默认的超平坦设置,以便更容易观察。
- 在聊天框中输入/test runall来运行测试。结果将通过聊天框或信标通知您。
在服务器上运行
请运行./gradlew runGameTest命令,所有的测试将自动进行。
结果将以以下方式显示在日志中。
========= 2 GAME TESTS COMPLETE ======================
All 2 required tests passed :)
====================================================
如果gradlew在保存了守护进程日志的同时失败,但测试似乎是成功的,则可以像上面的示例一样添加forceExit false来解决该问题。
总结
所需之物 zhī wù)
build.gradle中の設定
CIで動かすなどヘッドレスでテストしたい場合
Javaのテストコード
クラスには@GameTestHolder(ExampleMod.MODID)のアノテーション
メソッドには@GameTestのアノテーション
アノテーションの詳細は後に記載
snbtファイル
run/gameteststructuresに設置
runはrunClientやrunGameTestServerの作業フォルダ
snbtファイルの作成法はtestコマンドの詳細に記載
文件夹结构
.
├── build.gradle
├── run (runClientやrunGameTestServerのworking directory)
│ └── gameteststructures
│ └── registerviaannotation.placedirt.snbt
└── src (ソースコード Java以下は任意です)
└── main
└── java
└── com
└── example
└── examplemod
└── gametest
└── RegisterViaAnnotation.java
考试的写法 de
添加考试的方法
@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のアノテーションにはテストの実行に関する様々なパラメータを指定できます。詳細はここを見てください。
在@GameTestGenerator上的新增功能
@GameTestGenerator是应用于方法的注解。
-
- staticでもinstanceメソッドでもOK
instanceメソッドの場合,インスタンスがテスト実行前に作られる
その場合リフレクションで引数なしのコンストラクタが呼び出される
可視性はpublic
引数はなし
返り値の型はCollection
ListなどのCollectionの子クラスならOK
在TestFunction中,您可以指定测试参数并将执行内容作为Consumer传递。有关传递参数的详细信息,请查看此处。
子:大哥,你知道明天有部好电影上映吗?
哥:不知道,我还没有听说过。
子:哎呀,那我们明天晚上做点别的吧。
@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コマンドの説明を参照
setupTicks: long
default: 0L
構造体が設置されてからテストが実行されるまでの間隔
template: String
default: “”
テストの構造体の名前
詳細はここ
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
テストの本体
关于结构体的名称
可使用@GameTest模板或TestFunction的structureName来指定结构体的名称。
在@GameTest中的结构体名称
决定命名结构体的元素如下所示。
@GameTestのtemplate
@GameTestHolderの値
または@GameTestのtemplateNamespace
@PrefixGameTestTemplateの値
构造体的名称采用ResourceLocation的格式(namespace:path)。
-
- namespaceは@GameTestHolderで指定した名前が入ります。@GameTestのtemplateNamespaceがある場合はこちらが優先されます。どちらもない場合は”minecraft”となります。
-
- pathは”.”の名前が入ります。
@GameTestでtemplateを指定しないか””とした場合は代わりにメソッド名を小文字にしたものがになります。
@PrefixGameTestTemplateのアノテーションでfalseを指定した場合はpathが””のみとなります。
このアノテーションはクラスかメソッドにつけられます。クラスにつけた場合はそのクラスのメソッドすべてが対象になります。
アノテーションをつけなかった場合はtrueとみなされ””が付加されます。
クラス名に関係ないファイル名が使えるため他クラスとの構造体ファイルの共有が可能になります。
上のnamespaceとpathからnamespace:pathとして決定されます
这个房间很大。
这个房间十分宽敞。
@GameTestHolder("m1")
class C1 {
@GameTest()
public void testNoArguments(GameTestHelper helper) {}
}
m1:c1.testnoarguments
@GameTestHolder("m1")
class C1 {
@GameTest(template="custom")
public void testCustomTemplate(GameTestHelper helper) {}
}
m1:c1.custom
@GameTestHolder("m1")
class C1 {
@GameTest(template="custom", templateNamespace="mod2")
public void testCustomNamespace(GameTestHelper helper) {}
}
mod2:c1.custom
@GameTestHolder("m1")
@PrefixGameTestTemplate(value = false)
class C1 {
@GameTest(template="custom")
public void testNoPrefixAndCustomName(GameTestHelper helper) {}
}
m1:custom
TestFunctionでの構造体の名前
传递的结构名将直接使用。
决定使用的结构体
まず,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といったように次の番号のバッチが作成されます。
前处理和后处理
您可以在每个批次中分别进行一次前处理和后处理。
前処理には@BeforeBatch(batch = “batchName”)のアノテーションをつけます。@GameTestと同様staticでもインスタンスメソッドでも使えます。メソッドの引数はServerLevelが1つです。
同じバッチに対して複数の@BeforeBatchが存在する場合には登録の時点でエラーになります。
後処理には@AfterBatch(batch = “batchName”)のアノテーションをつけます。
请提供更多的上下文或者完整的句子,以便为您提供准确的翻译。
@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的实例,并直接进行操作。
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的操作。
请参阅
In this video, the concept of Game Test is shown.
https://github.com/MinecraftForge/MinecraftForge
The GitHub code of Minecraft Forge.