Anvil
WorkflowsTesting

Assertions and waits

Connect asynchronous actions to precise observations and avoid matching old output.

Make each asynchronous step establish the fact needed by the next step. Starting login, sending a command, and requesting an interaction do not imply their server-side effects have completed.

Choose the right observation

Expected resultWait
Native login completedSession.connected()
Current session disconnectedSession.disconnected()
Server kicked the session with a reasonSession.kicked()
Player's chat showed textMessages.received(text)
Player joined a server or proxyPlayerWatch.joined(player) on that process's Players.watch()
Player left a server or proxyPlayerWatch.left(player)
Proxy connected the player to a backendPlayerWatch.joined(player, backend) on the proxy's Players.watch()
Proxy moved the player away from a backendPlayerWatch.left(player, backend) on the proxy's watch
Inventory reached a conditionInventory.matching(predicate)
New process output contains textConsoleWatch.printed(text, timeout)

Capability waits offer explicit duration overloads; their default maximum wait is ten seconds. Immediate snapshots such as Session.state(), Messages.history(), Players.profile(player), Players.backend(player), and Inventory.snapshot() do not wait. Players.backend(player) can lag a moment behind a move, so wait for the move on the proxy's watch instead of reading it straight after an action. Create a PlayerWatch before the action it observes; see watching players on a process.

Wait for whichever outcome comes first

Some tests have more than one correct outcome, decided by a race the test cannot order, such as the same player acting through two proxies at once. Waiting for one outcome would then time out whenever the other one happens. SimulatedPlayer.awaitFirst waits for the first of several expectations and returns its index; the capability's ordinary wait for that outcome then returns at once:

Messages messages = player.capability(Messages.class);
Session session = player.capability(Session.class);
MessageWatch chat = messages.watch();
messages.command("confirm 123456");

int outcome = player.awaitFirst(Duration.ofSeconds(10),
        chat.expectReceived(text -> text.contains("Invalid code")),
        session.expectKicked());
if (outcome == 1) assertTrue(session.kicked().contains("Reconnect"));
ExpectationMet when
MessageWatch.expectReceived(matcher)a message the chat showed since the watch's previous wait matches
DialogWatch.expectShown(matcher)a dialog shown since the watch's previous wait matches
CookieWatch.expectRequested(key)the server asked for the cookie since the watch's previous wait
TitleWatch.expectShown(matcher), ActionBarWatch.expectShown(matcher)a title or action bar shown since the watch's previous wait matches
BossBarWatch.expectShown(matcher), BossBarWatch.expectRemoved(matcher)a boss bar added or changed, or removed, since the watch's previous wait matches
SoundWatch.expectHeard(matcher)a sound heard since the watch's previous wait matches
Session.expectKicked()the server kicked the current session
Session.expectDisconnected()the current session is disconnected for any reason
PlayerWatch.expectJoined(player)the player joined that watch's process since the watch's previous wait
PlayerWatch.expectJoined(player, backend)the proxy connected the player to that backend since the watch's previous wait
Expectation.of(description, condition)a check of your own that never waits holds

When several expectations are met by the time they are checked, the earliest argument wins. A timeout names every expectation, joined by "or". Use it only for outcomes that are really both correct; one expected result still belongs in an ordinary wait, which explains a failure better.

Watch console output before acting

Inside a live test with ScenarioContext anvil and the first-test process named server, import java.time.Duration and use:

var console = anvil.processes().server("server").console();
var output = console.watch();
console.sendCommand("say watch-verification");
String line = output.printed("watch-verification", Duration.ofSeconds(10));
output.neverPrinted("Exception", Duration.ofSeconds(2));

Start the watch before the command. The returned line must have been printed after the watch started, so an earlier identical message cannot satisfy the wait, and each later wait continues after it. A watch belongs to one process console; watch the new console after a process restart.

Avoid old messages satisfying new steps

Messages.received(text) searches the whole history of the chat. If you send the same command twice and wait for the same text, the first reply can satisfy both waits. Use a unique request/response token, a state change your application exposes, or a new player where a fresh history fits the journey.

Similarly, Inventory.matching(...) can return immediately when the current snapshot already matches. Write a condition that distinguishes the intended result from the initial state.

Assert application behavior

The first test establishes connection, observed presence, and message delivery. A plugin test should go further: trigger its behavior and assert the response, identity, route, or application state it promises. Reconnection alone does not demonstrate persistence.

Use JUnit assertions on the observed result. Avoid fixed sleeps as a substitute for observation: they neither establish completion nor explain which result was missing.

Diagnose a timeout

Check the nearest evidence first: connection state, received messages, the players the expected process reports, then the relevant process console tail. A command error or missing permission needs setup corrected, not a longer wait. Increase an explicit timeout only when the operation legitimately needs more time. Use troubleshooting for startup, routing, and retained diagnostics.

On this page