Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Game tests

Verified against Minecraft 26.2 · Part XIII · Run /test run * on a vanilla server and one test runs, and passes. The suite is not in the game — a test is a data-pack file, the Java body is a value the JSON points at, and the shipped jar declares exactly one of each.

Game tests are how Mojang checks that a piston still pushes and a hopper still pulls: a small structure is pasted into a spare corner of a world, a test body runs against it for a bounded number of ticks, and a block beside it turns green or red. That much has been true for years. What changed is where a test lives.

There is no GameTest annotation any more, and no test registry class. A test is a registry element loaded from data/<ns>/test_instance/, and the Java body — when there is one — is a value in a second registry, Registries.TEST_FUNCTION, that the JSON points at. The Java half is a payload, not the declaration. Which means the shipped jar contains GameTestInstances’ single always-pass instance, BuiltinTestFunctions’ body for it, and GameTestEnvironments’ default environment — an empty TestEnvironmentDefinition.AllOf — and the real suite lives in Mojang’s test sources, not in the game you downloaded.

This is the data-driven type pattern again, and game tests are its most complete instance: two data-pack registries and two built-in type registries between them.

The cast

classwhat it decides
GameTestInstancethe registry element. GameTestInstance.run takes a GameTestHelper and is the body — BlockBasedTestInstance needs no Java at all, FunctionGameTestInstance invokes a Registries.TEST_FUNCTION entry
TestDatathe declaration record every instance delegates to: environment, structure, tick budgets, required, rotation, manual-only, the two retry counts, sky access, padding
TestEnvironmentDefinitionthe seven ways to bend the world for a test, shaped as an undo log
GameTestBatcha group of tests keyed by their environment holder. A batch is an environment
GameTestRunnerowns the batches and the structure spawner, and re-queues a failure when the retry options say so
GameTestInfoone run of one test: its position, its timeout, its sequences, its outcome
GameTestHelper1,353 lines, and the entire surface a test body sees — coordinate translation, world edits, spawning, assertions, outcomes
TestInstanceBlockEntity551 lines: the block that owns a test’s bounding box, status and beacon beam, and does the real work of placing, saving and encasing the structure

net/minecraft/gametest/framework is forty-four classes, all server-side, with net/minecraft/gametest/Main as the headless entry point beside it.

The objects, and how they nest

flowchart TB
    subgraph D["THE DECLARATION — data pack files"]
        TI["GameTestInstance — data/ns/test_instance"]
        TD["TestData — environment, structure, tick budgets, retries"]
        TE["TestEnvironmentDefinition — data/ns/test_environment"]
        ST["a structure — data/ns/structure"]
        TI --> TD
        TD --> TE
        TD --> ST
    end
    subgraph R["THE RUN — one object per attempt"]
        GB["GameTestBatch — every test sharing ONE environment holder, split into runs of fifty"]
        GI2["GameTestInfo — one run of one test: position, timeout, sequences, outcome"]
        GH["GameTestHelper — test-local coordinates, edits, assertions"]
        GB --> GI2
        GI2 --> GH
    end
    subgraph W["THE WORLD — what a test costs"]
        TIB2["TestInstanceBlockEntity — bounding box, status, beacon beam, barrier shell, forced chunk"]
        TB2["TestBlock — start, log, fail and accept, for a test written with no Java"]
    end
    D --> R
    R --> W

A batch is not a name and not a class: it is an environment. GameTestBatchFactory groups tests by their TestEnvironmentDefinition holder, because that is what GameTestBatch is keyed by, and each group is split into runs of fifty (a default the builder can change, not a cap). One environment is active at a time on the runner, and moving between batches tears the old one down and stands the new one up.

The environment interface is an undo log. TestEnvironmentDefinition.setup returns a value that TestEnvironmentDefinition.teardown is handed back. Five of the seven kinds return the previous state and restore it; TestEnvironmentDefinition.Functions returns nothing and runs a different data-pack function on the way out; and TestEnvironmentDefinition.AllOf returns its children’s activations and unwinds them in reverse.

The trace: one test runs

sequenceDiagram
    participant TC as TestCommand
    participant GTR as GameTestRunner
    participant TIB as TestInstanceBlockEntity
    participant GTT as GameTestTicker
    participant GI as GameTestInfo
    participant RGL as ReportGameListener

    TC->>GTR: build one GameTestInfo per test, batched by environment
    GTR->>GI: spawn each info — prepareTestStructure
    GI->>TIB: placeStructure, then encaseStructure — a barrier shell round the test
    GTR->>GTR: TestEnvironmentDefinition.setup — returns the undo log
    GTR->>GTT: add every info whose structure was placed to the ticker
    GTT->>GI: tick — counting up from NEGATIVE: the setup ticks run before tick zero
    GI->>GI: GameTestInstance.run(helper) at tick zero, and sequences tick after
    GI->>RGL: succeed, or a GameTestException — a timeout is just another one
    RGL->>TIB: setSuccess or setErrorMessage — the beam turns green, red or orange
    RGL->>RGL: say to chat, and GlobalTestReporter to the log or to JUnit XML

Game tests tick on the server thread, from MinecraftServer.tickChildren in a profiler section named after the subsystem — after connections and players and the debug subscribers, before the server GUI refresh and chunk sending — and only when the tick-rate manager reports the game running normally, so /tick freeze suspends them.

Setup ticks run before tick zero. GameTestInfo.startExecution starts its counter negative — by the declared setup ticks, plus the spawner’s own tick delay, plus one — so the body runs when the count reaches zero. GameTestSequence is the “do this, wait, then assert that” chain — GameTestSequence.thenExecuteAfter, GameTestSequence.thenWaitUntil, GameTestSequence.thenSucceed — and it uses an exception as ordinary control flow, at most one thrown and swallowed per sequence per tick, and only for an assertion failure. A timeout is not caught there.

Reporting is a listener chain, and it writes to four places. ReportGameListener is what says something in chat and what writes the outcome back to the TestInstanceBlockEntity that owns the beam; MultipleTestTracker is the progress bar, with five states including a space for not started; and GlobalTestReporter dispatches to LogTestReporter or JUnitLikeTestReporter.

A test with no Java in it

BlockBasedTestInstance runs a test built entirely from TestBlocks inside the structure. TestBlockMode has four values — start, log, fail and accept — and the rules are as simple as they sound: exactly one start block emits redstone to begin, an accept block being triggered is a pass, and a fail block being triggered is a failure carrying its stored message. That is a unit test authored in-game with a redstone circuit and shipped as a structure plus a JSON.

The client half the framework’s package list hides is what makes that practical: TestInstanceBlockEditScreen and TestBlockEditScreen are how a test is authored in game, TestInstanceRenderer draws the bounding box, and GameTestBlockHighlightRenderer is the sole consumer of ClientboundGameTestHighlightPosPacket. Both serverbound test packets are sent by the client, from those screens: this is the one system in the part whose declaration a client edits, though far from the only one whose client talks back — commands, suggestions, dialog clicks and advancement tab switches are all serverbound too.

Two things a running server should know

/test exists on every server, not only in a development environment. Only the export subcommands are gated on running from an IDE. The command sits at Commands.LEVEL_GAMEMASTERS like every other data-pack command in this part.

Test instance blocks are points of interest. Locating every test within a 250-block radius is a POI query, not a block scan (points of interest), which is what makes /test’s radius subcommands cheap — and what makes a world full of saved tests carry them in its POI storage.

Underneath both, StructureUtils and StructureGridSpawner are the layer that clears the space, lays tests out in a grid, transforms the far corner and finds every test block by position (jigsaw and templates owns the template machinery they call). GameTestServer is a whole MinecraftServer subclass for headless runs, driven by GameTestMainUtil, and it overrides GameTestServer.waitUntilNextTick to drain tasks instead of sleeping: the headless test server runs flat out, and installs a no-op gizmo collector so debug drawing costs nothing (what this book skips).

Where to look

GameTestInstance and TestData for what a test is, then GameTestInfo for what actually happens on a tick, then TestInstanceBlockEntity for what a test costs the world, and GameTestHelper when you want to write one.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.