XMage ships a dedicated test module,Documentation Index
Fetch the complete documentation index at: https://mintlify.com/magefree/mage/llms.txt
Use this file to discover all available pages before exploring further.
Mage.Tests, that lets you simulate complete game states in JUnit without starting a server or a client. You set up what is in each player’s hand, battlefield, graveyard, and library; declare what actions each player takes and in which phase; then call execute() to run the simulation and assert the resulting state with purpose-built assertion helpers. This approach covers the vast majority of card-correctness issues before any human playtesting occurs.
Project Layout
Tests live underMage.Tests/src/test/java/org/mage/test/ and are organised by concern:
| Path | Contents |
|---|---|
org/mage/test/cards/ | Card-specific tests grouped by mechanic (abilities/, combat/, triggers/, etc.) |
org/mage/test/cards/single/<set-code>/ | Per-card test classes, one per card, one class per set code folder |
org/mage/test/AI/basic/ | AI behaviour tests (attack decisions, targeting, simulation stability) |
org/mage/test/combat/ | Combat-specific tests |
org/mage/test/commander/ | Commander-format tests |
org/mage/test/multiplayer/ | Multiplayer-specific tests |
org/mage/test/serverside/ | Base classes and framework infrastructure |
CardTestPlayerBase, which sets up a two-player duel (TwoPlayerDuel) with playerA and playerB using the default test deck ("RB Aggro.dck").
Base Test Class: CardTestPlayerBase
playerA and playerB are TestPlayer instances exposed as protected fields in CardTestPlayerAPIImpl. Every setup and assertion method accepts either playerA or playerB as its first argument.
Key Test API Methods
Setting Up Game State
All setup calls must be made beforeexecute(). They are queued and applied when the simulation begins.
| Method | Description |
|---|---|
addCard(Zone zone, TestPlayer player, String cardName) | Put one copy of a card into a zone |
addCard(Zone zone, TestPlayer player, String cardName, int count) | Put count copies into a zone |
addCard(Zone zone, TestPlayer player, String cardName, int count, boolean tapped) | Put cards into a zone, optionally tapped |
skipInitShuffling() | Disable the initial library shuffle so addCard(Zone.LIBRARY, ...) cards appear in the order you add them |
Zone enum values used most often are Zone.BATTLEFIELD, Zone.HAND, Zone.GRAVEYARD, Zone.LIBRARY, and Zone.EXILE.
Declaring Player Actions
Actions are also queued beforeexecute(). The turnNum parameter refers to the overall turn count (turn 1 is playerA’s first turn, turn 2 is playerB’s first turn, and so on).
| Method | Description |
|---|---|
castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName) | Cast a spell with no targets |
castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, String targetName) | Cast a spell targeting a named permanent or player |
castSpell(int turnNum, PhaseStep step, TestPlayer player, String cardName, Player target) | Cast targeting a player object |
activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability) | Activate an ability whose rule text starts with ability |
activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String... targetNames) | Activate an ability with one or more targets |
attack(int turnNum, TestPlayer player, String attacker) | Declare a creature as attacker (attacks the opponent by default) |
attack(int turnNum, TestPlayer player, String attacker, TestPlayer defendingPlayer) | Declare an attacker aimed at a specific player |
block(int turnNum, TestPlayer player, String blocker, String attacker) | Declare a blocker |
setChoice(TestPlayer player, String choice) | Supply a named choice (for modal spells, discard prompts, etc.) |
addTarget(TestPlayer player, String targetName) | Add an additional target for a queued action |
waitStackResolved(int turnNum, PhaseStep step, TestPlayer player) | Wait for the current stack to fully resolve before continuing |
Controlling Simulation Flow
| Method | Description |
|---|---|
setStopAt(int turnNum, PhaseStep step) | Tell the simulator where to stop (required before execute()) |
execute() | Run the game simulation up to the stop point |
setStrictChooseMode(true) | Fail the test if any unexpected choice prompt appears (recommended) |
Assertion Methods
All assertions are called afterexecute().
| Method | Description |
|---|---|
assertPermanentCount(Player player, String cardName, int count) | Number of named permanents on the battlefield |
assertPermanentCount(Player player, int count) | Total number of permanents on the battlefield |
assertGraveyardCount(Player player, String cardName, int count) | Number of named cards in graveyard |
assertGraveyardCount(Player player, int count) | Total graveyard size |
assertHandCount(Player player, String cardName, int count) | Named cards in hand |
assertHandCount(Player player, int count) | Total hand size |
assertLife(Player player, int life) | Player’s current life total |
assertTapped(String cardName, boolean tapped) | Whether a named permanent is tapped |
assertTappedCount(String cardName, boolean tapped, int count) | How many copies of a permanent are (un)tapped |
assertPowerToughness(Player player, String cardName, int power, int toughness) | Current P/T of a permanent |
assertCounterCount(Player player, String cardName, CounterType type, int count) | Counters on a named permanent |
assertCounterCount(Player player, CounterType type, int count) | Counters of a type on a player |
assertAbility(Player player, String cardName, Ability ability, boolean mustHave) | Whether a permanent has a specific ability |
assertTokenCount(Player player, String tokenName, int count) | Number of tokens with a given name |
assertType(String cardName, CardType type, boolean mustHave) | Card type assertion |
assertColor(Player player, String cardName, String searchColors, boolean mustHave) | Color assertion |
Writing a Basic Card Test
Place the file in
Mage.Tests/src/test/java/org/mage/test/cards/single/<set-code>/. Use the set code as the subdirectory (e.g. m14 for Magic 2014). Follow the naming convention <ClassName>Test.java:package org.mage.test.cards.single.m14;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class LightningBoltTest extends CardTestPlayerBase {
// test methods go here
}
Call
setStrictChooseMode(true) at the start of every test method. This turns unexpected choice prompts into test failures instead of hangs, making it much easier to diagnose missing setChoice() or addTarget() calls.Use
addCard(...) to place all relevant cards into the appropriate zones. Add mana sources to the battlefield so the player can actually cast spells:addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); // 2/2 creature
Declare what happens and when. Turn 1 is playerA’s first turn;
PhaseStep.PRECOMBAT_MAIN is the first main phase:Complete Example Tests
Running Tests
Run the entire test suite from the repository root:Mage.Tests module:
The test framework boots a lightweight in-process game engine. No server process or network connection is required. Tests are self-contained and run entirely in the Maven Surefire plugin.
Interactive Testing with Test Mode
For manual verification of complex interactions, start the XMage server with-DtestMode=true. In test mode, special cheat commands are enabled in the client that allow you to:
- Add any card to your hand mid-game
- Set your life total
- Fast-forward to a specific turn or phase
- Observe internal game state via the server console
Using the gen-card-test.pl Script
Just as gen-card.pl scaffolds a card class, gen-card-test.pl (also in Utils/) generates a test skeleton:
Mage.Tests/src/test/java/org/mage/test/cards/single/<set-code>/ and includes the correct package declaration, imports, class name, and one placeholder @Test method with setStrictChooseMode(true) already wired in, matching the template in Utils/cardTest.tmpl.
