Anvil
Building blocksEnvironments

Definition

Declare a repeatable environment and prepare its live state before a test or manual session begins.

An environment definition describes the starting point that an automated test or a prepared scenario needs. Declare its servers, proxies, and file inputs first. Add a setup hook when the starting point also requires work inside the running environment, such as connecting a participant or issuing an application setup command.

The definition returns an AnvilScenario; creating that declaration does not start it. The engine prepares the inputs and runs the setup hook when a workflow starts the environment.

Define the starting point

This example prepares one Paper server and a connected player named GuideBot. A test can use that player in its journey, or a human can join a manual variant with the participant already present.

Use the dependencies from Installation: Paper, MCProtocol, and the Session and Players capabilities, which the explicitly selected default capability unit includes. Save the class as com/example/test/PreparedEnvironment.java in the source set used by your integration. With the Anvil Gradle plugin, the complete path is src/anvil/java/com/example/test/PreparedEnvironment.java.

package com.example.test;

import me.whereareiam.anvil.api.model.process.Distribution;
import me.whereareiam.anvil.api.model.process.MinecraftServer;
import me.whereareiam.anvil.api.model.scenario.AnvilScenario;
import me.whereareiam.anvil.api.scenario.AnvilScenarioDefinition;
import me.whereareiam.anvil.api.type.Platforms;
import me.whereareiam.anvil.capability.players.PlayerWatch;
import me.whereareiam.anvil.capability.players.Players;
import me.whereareiam.anvil.capability.session.Session;
import org.jetbrains.annotations.NotNull;

public final class PreparedEnvironment implements AnvilScenarioDefinition {
	@Override
	public @NotNull AnvilScenario define() {
		MinecraftServer server = MinecraftServer.builder()
				.name("server")
				.platform(Platforms.PAPER)
				.distribution(Distribution.remote("1.21.11", "132"))
				.build();

		return AnvilScenario.builder()
				.name("prepared-paper")
				.entrypoint(server.getName())
				.server(server)
				.setupHook(access -> {
					PlayerWatch arrivals = access.processes().server(server.getName())
							.capability(Players.class).watch();
					var guide = access.players().create("GuideBot");
					var session = guide.capability(Session.class);
					session.connect();
					session.connected();
					arrivals.joined(guide);
				})
				.build();
	}
}

prepared-paper identifies the environment; server identifies the process within it. The entrypoint selects the default connection target for newly created players. Use distinct process names and select the proxy as the entrypoint when the behavior must pass through a proxy.

The setup waits establish a native connection and that the server reports GuideBot on it. They make GuideBot part of the starting state; they do not verify your plugin's behavior. A journey can retrieve the existing player with anvil.players().get("GuideBot"), or create another player with a different name. An environment that needs only processes can omit the setup hook.

Describe the scenario's purpose

Give a scenario a name that explains the behavior you want to investigate. Its description should explain the starting state, the important interaction, and the outcome a test or person should check. Server products and versions identify variants of that purpose; they rarely explain it on their own.

Put this information in the definition's PresentationMetadata. The IDE and runner read it from the discovered scenario. This fragment derives a labeled variant of PreparedEnvironment; import me.whereareiam.anvil.api.model.PresentationMetadata alongside AnvilScenario:

AnvilScenario labeled = new PreparedEnvironment().define().toBuilder()
		.metadata(PresentationMetadata.builder()
				.displayName("Joining an occupied server")
				.description("GuideBot is already connected. Use this starting state to check that a second "
						+ "player can join while the existing participant stays connected.")
				.category("Player connections")
				.tag("paper")
				.build())
		.build();

Its name remains prepared-paper. Metadata is optional, and each field is optional: tooling derives a label from the technical name when displayName is absent. Category and tags organize presentation without changing the topology or startup behavior.

MinecraftServer and MinecraftProxy builders accept the same metadata. Describe their role within the scenario: for example, a Lobby accepts the initial connection and a Game server receives the transfer. Their lookup and entrypoint names stay stable even when their display labels change.

For a plugin that replicates sessions across proxies through Redis, a useful purpose would be Session replication across proxies. The following metadata fragment assumes that replicationTopology is an existing AnvilScenario with the plugin, proxy topology, and Redis connection configuration already supplied by your integration:

AnvilScenario replication = replicationTopology.toBuilder()
		.name("cross-proxy-session-replication")
		.metadata(PresentationMetadata.builder()
				.displayName("Session replication across proxies")
				.description("Sign in through the first proxy, then reconnect through the second. "
						+ "Check that both proxies observe the same session through Redis and that logout "
						+ "on either proxy invalidates the session on the other.")
				.category("Session replication")
				.tag("redis")
				.build())
		.build();

The example adds presentation to that prepared topology. Your integration supplies Redis and owns its lifecycle. Keep the connection actions and bounded replication assertions in the test journey; starting the environment in the IDE prepares it for investigation. A Running status reports process readiness, not an assertion result.

Keep inputs in the declaration

Choose the platform required by the behavior, then provision its executable and a compatible Java runtime. Automated runs use an explicit provider build or a supported content checksum. A supplied local or named server JAR also declares minecraftVersion, so Anvil can validate compatible native clients.

Use a workspace plan for the packaged plugin, configuration files, and fixture data. Register the exact artifact through the integration or embedding application rather than selecting an unrelated file from another project's build directory. Files are prepared before platform configuration and process startup; they do not need a live setup hook.

Use the hook for operations that require ready processes or players. Keep the actions and assertions that define the actual test result in the testing journey. This lets several tests reuse the same prepared state while checking different behavior.

Join a network Anvil does not run

A scenario can name servers and proxies that run elsewhere, such as an existing network you have permission to test against. Declare each one as a RemoteEndpoint with its address, written as a player types it into the multiplayer screen, and the Minecraft version it speaks:

import me.whereareiam.anvil.api.model.scenario.RemoteEndpoint;

AnvilScenario scenario = AnvilScenario.builder()
		.name("public-network")
		.entrypoint("network")
		.remote(RemoteEndpoint.builder()
				.name("network")
				.address("play.example.com")
				.minecraftVersion("1.21.11")
				.build())
		.build();

A remote shares the namespace of the scenario's servers and proxies, so it can be the entrypoint or a player's connection target. A scenario may consist of remotes alone: it then starts no process and needs no EULA acceptance. Anvil starts, stops, configures and observes nothing on a remote's side, so a remote is not among processes(), has no console and no process capabilities, and a proxy cannot register it as a backend. Validation refuses a remote without a name that is unique among servers, proxies and remotes, an address that cannot be parsed, and a blank or invalid Minecraft version.

Prepare live state

When the environment starts, Anvil validates its declaration, prepares its inputs, starts servers before proxies, and establishes process and agent readiness. It then invokes the setup hook. The caller receives a ready environment only after that hook completes.

The callback receives ScenarioAccess, exposing the declaration, process lookup, and player manager. It does not receive a ScenarioContext to close. The engine and the caller's integration own that lifetime; see Running context.

Wait for the precondition your hook creates. The example uses Session and Players waits; a hook that sends console commands should use bounded console observations. Submitting a command alone does not establish that the requested setup finished.

If setup fails, startup fails rather than returning a ready context. Anvil attempts cleanup of the resources it acquired and preserves the original failure with cleanup failures attached. Use Startup troubleshooting to inspect the cause and logs.

The hook runs once for each whole-environment startup. A single-process restart does not rerun it. Starting the declaration again, including through the manual runner's whole-scenario restart, creates a new run and repeats setup. Retained files follow the workspace policy; retaining a directory does not retain its previous player objects.

Use the definition in either workflow

For an automated test, select PreparedEnvironment.class with @AnvilTest. For example, change the annotation in PlayerJoinTest from the first test, then run:

./gradlew anvilTest --tests 'com.example.test.PlayerJoinTest'

Before that test method starts, GuideBot is already connected. The method can create Alice and check its own results. JUnit's per-method creation and teardown are documented in scenario selection.

For human testing, create a separate definition class that derives a manual variant from the same environment. Keep the returned value in that definition; no registry or provider is required:

public final class PreparedPaperManualScenario implements AnvilScenarioDefinition {
	@Override
	public AnvilScenario define() {
		return new PreparedEnvironment().define().toBuilder()
				.name("prepared-paper-manual")
				.manual(true)
				.build();
	}
}

The compiled definition is indexed with the other scenarios. Launch prepared-paper-manual through the runner and join with your real client while GuideBot remains present. Manual mode does not install missing components or accept the EULA; the same declared dependencies and explicit runtime settings still apply.

On this page