13,640 bytes added
, 04:02, 20 April 2022
Game Tests are a way to run in-game unit tests. The system was designed to be scalable and in parallel to run large numbers of different tests efficiently. Testing object interactions and behaviors are simply a few of the many applications of this framework.
== Creating a Game Test ==
A standard Game Test follows three basic steps:
# A structure, or template, is loaded holding the scene on which the interaction or behavior is tested.
# A method conducts the logic to perform on the scene.
# The method logic executes. If a successful state is reached, then the test succeeds. Otherwise, the test fails and the result is stored within a lectern adjacent to the scene.
As such, to create a Game Test, there must be an existing template holding the initial start state of the scene and a method which provides the logic of execution.
=== The Test Method ===
A Game Test method is a <code>Consumer<GameTestHelper></code> reference, meaning it takes in a <code>GameTestHelper</code> and returns nothing. For a Game Test method to be recognized, it must have a <code>@GameTest</code> annotation:
<syntaxhighlight lang="java">
public class ExampleGameTests {
@GameTest
public static void exampleTest(GameTestHelper helper) {
// Do stuff
}
}
</syntaxhighlight>
The <code>@GameTest</code> annotation also contains members which configure how the game test should run.
<syntaxhighlight lang="java">
// In some class
@GameTest(
setupTicks = 20L, // The test spends 20 ticks to set up for execution
required = false // The failure is logged but does not affect the execution of the batch
)
public static void exampleConfiguredTest(GameTestHelper helper) {
// Do stuff
}
</syntaxhighlight>
==== Relative Positioning ====
All <code>GameTestHelper</code> methods translate relative coordinates within the structure template scene to its absolute coordinates using the structure block's current location. To allow for easy conversion between relative and absolute positioning, <code>GameTestHelper#absolutePos</code> and <code>GameTestHelper#relativePos</code> can be used respectively.
The relative position of a structure template can be obtained in-game by loading the structure via the [[#Running_Game_Tests|test command]], placing the player at the wanted location, and finally running the <code>/test pos</code> command. This will grab the coordinates of the player relative to the closest structure within 200 blocks of the player. The command will export the relative position as a copyable text component in the chat to be used as a final local variable.
{{Tip|The local variable generated by <code>/test pos</code> can specify its reference name by appending it to the end of the command:
<syntaxhighlight lang="powershell">
/test pos <var> # Exports 'final BlockPos <var> = new BlockPos(...);'
</syntaxhighlight>
}}
==== Successful Completion ====
A Game Test method is responsible for one thing: marking the test was successful on a valid completion. If no success state was achieved before the timeout is reached (as defined by <code>GameTest#timeoutTicks</code>), then the test automatically fails.
There are many abstracted methods within <code>GameTestHelper</code> which can be used to define a successful state; however, four are extremely important to be aware of.
{| class="wikitable"
|-
! Method !! Description
|-
| <code>#succeed</code> || The test is marked as successful.
|-
| <code>#succeedIf</code> || The supplied <code>Runnable</code> is tested immediately and succeeds if no <code>GameTestAssertException</code> is thrown. If the test does not succeed on the immediate tick, then it is marked as a failure.
|-
| <code>#succeedWhen</code> || The supplied <code>Runnable</code> is tested every tick until timeout and succeeds if the check on one of the ticks does not throw a <code>GameTestAssertException</code>.
|-
| <code>#succeedOnTickWhen</code> || The supplied <code>Runnable</code> is tested on the specified tick and will succeed if no <code>GameTestAssertException</code> is thrown. If the <code>Runnable</code> succeeds on any other tick, then it is marked as a failure.
|}
{{Tip/Important|Game Tests are executed every tick until the test is marked as a success. As such, methods which schedule success on a given tick must be careful to always fail on any previous tick.}}
==== Scheduling Actions ====
Not all actions will occur when a test begins. Actions can be scheduled to occur at specific times or intervals:
{| class="wikitable"
|-
! Method !! Description
|-
| <code>#runAtTickTime</code> || The action is ran on the specified tick.
|-
| <code>#runAfterDelay</code> || The action is ran <code>x</code> ticks after the current tick.
|-
| <code>#onEachTick</code> || The action is ran every tick.
|}
==== Assertions ====
At any time during a Game Test, an assertion can be made to check if a given condition is true. There are numerous assertion methods within <code>GameTestHelper</code>; however, it simplifies to throwing a <code>GameTestAssertException</code> whenever the appropriate state is not met.
=== Generated Test Methods ===
If Game Test methods need to be generated dynamically, a test method generator can be created. These methods take in no parameters and return a collection of <code>TestFunction</code>s. For a test method generator to be recognized, it must have a <code>@GameTestGenerator</code> annotation:
<syntaxhighlight lang="java">
public class ExampleGameTests {
@GameTestGenerator
public static Collection<TestFunction> exampleTests() {
// Return a collection of TestFunctions
}
}
</syntaxhighlight>
==== TestFunction ====
A <code>TestFunction</code> is the boxed information held by the <code>@GameTest</code> annotation and the method running the test.
{{Tip|Any methods annotated using <code>@GameTest</code> are translated into a <code>TestFunction</code> using <code>GameTestRegistry#turnMethodIntoTestFunction</code>. That method can be used as a reference for creating <code>TestFunction</code>s without the use of the annotation.}}
=== Batching ===
Game Tests can be executed in batches instead of registration order. A test can be added to a batch by having the same supplied <code>GameTest#batch</code> string.
On its own, batching does not provide anything useful. However, batching can be used to perform setup and teardown states on the current level the tests are running in. This is done by annotating a method with either <code>@BeforeBatch</code> for setup or <code>@AfterBatch</code> for takedown. The `#batch` methods must match the string supplied to the game test.
Batch methods are <code>Consumer<ServerLevel></code> references, meaning they take in a <code>ServerLevel</code> and return nothing:
<syntaxhighlight lang="java">
public class ExampleGameTests {
@BeforeBatch(batch = "firstBatch")
public static void beforeTest(ServerLevel level) {
// Perform setup
}
@GameTest(batch = "firstBatch")
public static void exampleTest2(GameTestHelper helper) {
// Do stuff
}
}
</syntaxhighlight>
== Registering a Game Test ==
A Game Test must be registered to be ran in-game. There are two methods of doing so: via the <code>@GameTestHolder</code> annotation or <code>RegisterGameTestsEvent</code>. Both registration methods still require the test methods to be annotated with either <code>@GameTest</code>, <code>@GameTestGenerator</code>, <code>@BeforeBatch</code>, or <code>@AfterBatch</code>.
=== GameTestHolder ===
The <code>@GameTestHolder</code> annotation registers any test methods within the type (class, interface, enum, or record). <code>@GameTestHolder</code> contains a single method which has multiple uses. In this instance, the supplied <code>#value</code> must be the mod id of the mod; otherwise, the test will not run under default configurations.
<syntaxhighlight lang="java">
@GameTestHolder(MODID)
public class ExampleGameTests {
// ...
}
</syntaxhighlight>
=== RegisterGameTestsEvent ===
<code>RegisterGameTestEvent</code> can also register either classes or methods using <code>#register</code>. The event listener must be [[Events#Event_Listeners|added]] to the mod event bus. Test methods registered this way must supply their mod id to <code>GameTest#templateNamespace</code> on every method annotated with <code>@GameTest</code>.
<syntaxhighlight lang="java">
// In some class
public void registerTests(RegisterGameTestsEvent event) {
event.register(ExampleGameTests.class);
}
// In ExampleGameTests
@GameTest(templateNamespace = MODID)
public static void exampleTest3(GameTestHelper helper) {
// Perform setup
}
</syntaxhighlight>
{{Tip|The value supplied to <code>GameTestHolder#value</code> and <code>GameTest#templateNamespace</code> can be different from the current mod id. The configuration within the [[#Enabling_Other_Namespaces|buildscript]] would need to be changed.}}
== Structure Templates ==
Game Tests are performed within scenes loaded by structures, or templates. All templates define the dimensions of the scene and the initial data (blocks and entities) that will be loaded. The template must be stored as an <code>.nbt</code> file within <code>data/<namespace>/structures</code>.
{{Tip|A structure template can be created and saved using a structure block.}}
The location of the template is specified by a few factors:
* If the namespace of the template is specified.
* If the class should be prepended to the name of the template.
* If the name of the template is specified.
The namespace of the template is determined by <code>GameTest#templateNamespace</code>, then <code>GameTestHolder#value</code> if not specified, then <code>minecraft</code> if neither is specified.
The simple class name is not prepended to the name of the template if the <code>@PrefixGameTestTemplate</code> is applied to a class or method with the test annotations and set to <code>false</code>. Otherwise, the simple class name is made lowercase and prepended and followed by a dot before the template name.
The name of the template is determined by <code>GameTest#template</code>. If not specified, then the lowercase name of the method is used instead.
<syntaxhighlight lang="java">
// Modid for all structures will be MODID
@GameTestHolder(MODID)
public class ExampleGameTests {
// Class name is prepended, template name is not specified
// Template Location at 'modid:examplegametests.exampletest'
@GameTest
public static void exampleTest(GameTestHelper helper) { /*...*/ }
// Class name is not prepended, template name is not specified
// Template Location at 'modid:exampletest2'
@PrefixGameTestTemplate(false)
@GameTest
public static void exampleTest2(GameTestHelper helper) { /*...*/ }
// Class name is prepended, template name is specified
// Template Location at 'modid:examplegametests.test_template'
@GameTest(template = "test_template")
public static void exampleTest3(GameTestHelper helper) { /*...*/ }
// Class name is not prepended, template name is specified
// Template Location at 'modid:test_template2'
@PrefixGameTestTemplate(false)
@GameTest(template = "test_template2")
public static void exampleTest4(GameTestHelper helper) { /*...*/ }
}
</syntaxhighlight>
== Running Game Tests ==
Game Tests can be run using the <code>/test</code> command. The <code>test</code> command is highly configurable; however, only a few are of importance to running tests:
{| class="wikitable"
|-
! Header text !! Header text
|-
| <code>run</code> || Runs the specified test: <code>run <test_name></code>.
|-
| <code>runall</code> || Runs all available tests.
|-
| <code>runthis</code> || Runs the nearest test to the player within 15 blocks.
|-
| <code>runthese</code> || Runs tests within 200 blocks of the player.
|-
| <code>runfailed</code> || Runs all tests that failed in the previous run.
|}
{{Tip|Subcommands follow the test command: <code>/test <subcommand></code>.}}
== Buildscript Configurations ==
Game Tests provide additional configuration settings within a buildscript (the <code>build.gradle</code> file) to run and integrate into different settings.
=== Enabling Other Namespaces ===
If the buildscript was [[Getting_Started#Customizing_the_MDK|setup as recommended]], then only Game Tests under the current mod id would be enabled. To enable other namespaces to load Game Tests from, a run configuration must set the property <code>forge.enabledGameTestNamespaces</code> to a string specifying each namespace separated by a comma. If the property is empty or not set, then all namespaces will be loaded.
<syntaxhighlight lang="gradle">
// Inside a run configuration
property 'forge.enabledGameTestNamespaces', 'modid1,modid2,modid3'
</syntaxhighlight>
{{Tip/Warning|There must be no spaces in-between namespaces; otherwise, the namespace will not be loaded correctly.}}
=== Game Test Server Run Configuration ===
The Game Test Server is a special configuration which runs a build server. The build server returns an exit code of the number of required, failed Game Tests. All failed tests, whether required or optional, are logged. This server can be run using <code>gradlew runGameTestServer</code>.
=== Enabling Game Tests in Other Run Configurations ===
By default, only the <code>client</code>, <code>server</code>, and <code>gameTestServer</code> run configurations have Game Tests enabled. If another run configuration should run Game Tests, then the <code>forge.enableGameTest</code> property must be set to <code>true</code>.
<syntaxhighlight lang="gradle">
// Inside a run configuration
property 'forge.enableGameTest', 'true'
</syntaxhighlight>