Testing
Fabric's client game tests start the real game with the client and the add-on, and drive it from code: create a world, enable modules, run commands, take screenshots. Extras runs every one of its modules this way, LiquidBounce tests itself the same way.
Setup
Fabric Loom creates a separate gametest source set, a mod of its own that is never part of the add-on's jar. In build.gradle.kts:
fabricApi {
configureTests {
createSourceSet = true
modId = "example-addon-gametest"
enableGameTests = false
}
}
loom.runs.named("clientGameTest") {
environmentVars.put("LB_BASIC_MODE", "true")
systemProperties.put("fabric.noGui", "true")
systemProperties.put(
"net.ccbluex.liquidbounce.browser.libraries",
gradle.gradleUserHomeDir.resolve("liquidbounce-gametest/mcef").path,
)
}
enableGameTests = falseleaves out Fabric's server game tests, the add-on is client-only.LB_BASIC_MODEkeeps Minecraft's own menus, which the test API navigates, instead of the client's web interface. The HUD stays the client's.fabric.noGuistops Fabric Loader from waiting on an error dialog nobody sees.- The run directory is wiped before every run. The browser the client downloads on its first start (about 140 MB) is kept outside of it through
net.ccbluex.liquidbounce.browser.libraries.
src/gametest/resources/fabric.mod.json declares the test:
{
"schemaVersion": 1,
"id": "example-addon-gametest",
"version": "1.0.0",
"name": "Example Addon Game Tests",
"environment": "client",
"entrypoints": {
"fabric-client-gametest": [
"com.example.addon.gametest.ExampleGameTest"
]
},
"depends": {
"example-addon": "*",
"fabric-client-gametest-api-v1": "*"
}
}
Writing a test
class ExampleGameTest : FabricClientGameTest {
override fun runTest(context: ClientGameTestContext) {
context.waitFor({ BrowserBackendManager.backend?.isInitialized == true }, 20 * 120)
context.worldBuilder().create().use { world ->
world.connection.waitForChunksRender()
val module = requireNotNull(ModuleManager["JumpCounter"]) { "JumpCounter is not registered" }
context.client { module.enabled = true }
context.waitTicks(20)
context.takeScreenshot("JumpCounter")
context.client { module.enabled = false }
context.client { CommandManager.execute("greet") }
context.waitTicks(2)
}
}
}
// The context's lambdas declare a throwable type Kotlin cannot infer.
private fun <T> ClientGameTestContext.client(block: (Minecraft) -> T): T =
computeOnClient<T, RuntimeException> { block(it) }
- The test runs on its own thread. Anything that touches the game or the client goes through
computeOnClientorrunOnClient, the server throughworld.server. - Waiting for the browser first lets the client finish starting.
ModuleManager[name]finds a module,CommandManager.executeruns a command without the prefix.- An exception fails the run. Extras also collects every logged error and fails on them, which catches problems inside handlers that the client only logs.
Running
./gradlew runClientGameTest
The game needs a display. Without one, for example in CI, run it under Xvfb. Minecraft asks for an sRGB framebuffer, which Xvfb's GLX does not offer, so SDL has to use EGL (libegl1 on Debian and Ubuntu):
SDL_VIDEO_FORCE_EGL=1 xvfb-run -a -s "-screen 0 1280x720x24" ./gradlew runClientGameTest
Screenshots land in build/run/clientGameTest/screenshots/.