Testing Library
Minestom provides a dedicated testing library that makes it easy to write integration tests for your server code. The library is available as a separate module:net.minestom:minestom-testing.
Adding the Testing Dependency
Add the testing library to your build configuration:// build.gradle.kts
dependencies {
testImplementation("net.minestom:minestom-testing:1.0.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
}
tasks.test {
useJUnitPlatform()
}
// build.gradle
dependencies {
testImplementation 'net.minestom:minestom-testing:1.0.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
}
test {
useJUnitPlatform()
}
The Minestom testing library is built on JUnit 5 and provides a custom test extension for managing server lifecycle.
Basic Test Structure
The foundation of Minestom testing is the@EnvTest annotation and the Env interface:
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class MyFirstTest {
@Test
public void testBasicServer(Env env) {
// The Env parameter is automatically injected
// It provides access to a test server instance
assertNotNull(env.process());
}
}
The
@EnvTest annotation automatically sets up and tears down a Minestom server for each test class. Each test method receives an Env instance.The Env Interface
TheEnv interface is your primary tool for interacting with the test environment:
Creating Test Instances
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
@EnvTest
public class InstanceTest {
@Test
public void testFlatInstance(Env env) {
// Create a flat instance filled with stone
Instance instance = env.createFlatInstance();
assertNotNull(instance);
assertEquals(Block.STONE, instance.getBlock(0, 0, 0));
}
@Test
public void testEmptyInstance(Env env) {
// Create a completely empty instance
Instance instance = env.createEmptyInstance();
assertNotNull(instance);
assertEquals(Block.AIR, instance.getBlock(0, 0, 0));
}
@Test
public void testCustomGenerator(Env env) {
// Create instance with custom generator
Instance instance = env.createFlatInstance(chunkLoader);
instance.setGenerator(unit -> {
unit.modifier().fillHeight(0, 10, Block.GRASS_BLOCK);
});
// Load a chunk to trigger generation
instance.loadChunk(0, 0).join();
assertEquals(Block.GRASS_BLOCK, instance.getBlock(0, 5, 0));
}
@Test
public void testCleanup(Env env) {
Instance instance = env.createEmptyInstance();
// Do testing...
// Clean up the instance when done
env.destroyInstance(instance);
}
}
Creating Test Players
import net.minestom.server.entity.Player;
import net.minestom.server.coordinate.Pos;
import net.minestom.testing.TestConnection;
@EnvTest
public class PlayerTest {
@Test
public void testPlayerCreation(Env env) {
Instance instance = env.createFlatInstance();
// Create a player at specific position
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
assertNotNull(player);
assertEquals(42, player.getPosition().y());
assertEquals(instance, player.getInstance());
}
@Test
public void testCustomConnection(Env env) {
// Create a connection with custom profile
TestConnection connection = env.createConnection(
new GameProfile(UUID.randomUUID(), "TestPlayer")
);
Instance instance = env.createFlatInstance();
Player player = connection.connect(instance, new Pos(0, 42, 0));
assertEquals("TestPlayer", player.getUsername());
}
@Test
public void testMultiplePlayers(Env env) {
Instance instance = env.createFlatInstance();
Player player1 = env.createPlayer(instance, new Pos(0, 42, 0));
Player player2 = env.createPlayer(instance, new Pos(10, 42, 10));
assertEquals(2, instance.getPlayers().size());
}
}
Test Connections
TestConnection objects represent fake clients that can interact with your server:
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.testing.Collector;
@EnvTest
public class ConnectionTest {
@Test
public void testPacketTracking(Env env) {
Instance instance = env.createFlatInstance();
TestConnection connection = env.createConnection();
// Track incoming packets of a specific type
Collector<ChunkDataPacket> packets = connection.trackIncoming(ChunkDataPacket.class);
// Connect player (this will trigger chunk packets)
Player player = connection.connect(instance, new Pos(0, 42, 0));
// Tick the server to process packets
env.tick();
// Verify packets were received
packets.assertAny(); // At least one packet
}
@Test
public void testAllPackets(Env env) {
Instance instance = env.createFlatInstance();
TestConnection connection = env.createConnection();
// Track all incoming packets
Collector<ServerPacket> packets = connection.trackIncoming();
Player player = connection.connect(instance, new Pos(0, 42, 0));
env.tick();
// Many packets should be sent on join
packets.assertAny();
}
}
Collectors
Collectors are powerful tools for gathering and asserting on collected data:import net.minestom.testing.Collector;
@EnvTest
public class CollectorTest {
@Test
public void testCollectorAssertions(Env env) {
Instance instance = env.createFlatInstance();
TestConnection connection = env.createConnection();
Collector<ChunkDataPacket> packets = connection.trackIncoming(ChunkDataPacket.class);
Player player = connection.connect(instance, new Pos(0, 42, 0));
env.tick();
// Assert exactly one packet
packets.assertSingle();
// Assert specific count
packets.assertCount(1);
// Assert at least one
packets.assertAny();
// Assert empty
Collector<SomeOtherPacket> empty = connection.trackIncoming(SomeOtherPacket.class);
empty.assertEmpty();
}
@Test
public void testCollectorPredicates(Env env) {
Instance instance = env.createFlatInstance();
TestConnection connection = env.createConnection();
Collector<ChunkDataPacket> packets = connection.trackIncoming(ChunkDataPacket.class);
Player player = connection.connect(instance, new Pos(0, 42, 0));
env.tick();
// Assert at least one matches predicate
packets.assertAnyMatch(packet ->
packet.chunkX() == 0 && packet.chunkZ() == 0
);
// Assert none match predicate
packets.assertNoneMatch(packet ->
packet.chunkX() == 999
);
// Assert all match predicate
packets.assertAllMatch(packet ->
packet.chunkX() >= -10 && packet.chunkX() <= 10
);
}
@Test
public void testCollectorConsumer(Env env) {
Instance instance = env.createFlatInstance();
TestConnection connection = env.createConnection();
Collector<ChunkDataPacket> packets = connection.trackIncoming(ChunkDataPacket.class);
Player player = connection.connect(instance, new Pos(0, 42, 0));
env.tick();
// Assert single packet and inspect it
packets.assertSingle(packet -> {
assertEquals(0, packet.chunkX());
assertEquals(0, packet.chunkZ());
});
}
}
Event Testing
Test that your code properly handles and fires events:import net.minestom.server.event.player.PlayerMoveEvent;
import net.minestom.server.event.EventFilter;
import net.minestom.testing.Collector;
@EnvTest
public class EventTest {
@Test
public void testPlayerMoveEvent(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Track events for a specific player
Collector<PlayerMoveEvent> events = env.trackEvent(
PlayerMoveEvent.class,
EventFilter.PLAYER,
player
);
// Move the player
player.teleport(new Pos(10, 42, 10));
env.tick();
// Verify event was fired
events.assertSingle(event -> {
assertEquals(player, event.getPlayer());
assertEquals(10, event.getNewPosition().x());
});
}
@Test
public void testCustomEvent(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Track custom event
Collector<MyCustomEvent> events = env.trackEvent(
MyCustomEvent.class,
EventFilter.PLAYER,
player
);
// Trigger the event
EventDispatcher.call(new MyCustomEvent(player));
events.assertSingle();
}
}
Flexible Event Listeners
For more complex event testing, useFlexibleListener:
import net.minestom.testing.FlexibleListener;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
@EnvTest
public class FlexibleListenerTest {
@Test
public void testEventModification(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Create a flexible listener
FlexibleListener<PlayerBlockBreakEvent> listener =
env.listen(PlayerBlockBreakEvent.class);
// Modify events
listener.on(event -> {
// Cancel all block breaks
event.setCancelled(true);
});
// Trigger block break
// ... your block break logic ...
// The event will be cancelled by the listener
}
}
Server Ticking
Control server ticking in your tests:@EnvTest
public class TickingTest {
@Test
public void testManualTick(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Apply velocity to player
player.setVelocity(new Vec(1, 0, 0));
// Tick the server once
env.tick();
// Player should have moved
assertTrue(player.getPosition().x() > 0);
}
@Test
public void testTickWhile(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
player.setVelocity(new Vec(1, 0, 0));
// Tick until condition is met or timeout
boolean completed = env.tickWhile(
() -> player.getPosition().x() < 10,
Duration.ofSeconds(5)
);
assertTrue(completed);
assertTrue(player.getPosition().x() >= 10);
}
@Test
public void testTimeout(Env env) {
Instance instance = env.createFlatInstance();
// This will timeout because condition never becomes false
boolean completed = env.tickWhile(
() -> true,
Duration.ofMillis(100)
);
assertFalse(completed); // Timed out
}
}
Testing Utilities
The testing library includes useful assertion utilities:import net.minestom.testing.TestUtils;
import net.minestom.server.coordinate.Point;
import net.kyori.adventure.nbt.CompoundBinaryTag;
@EnvTest
public class UtilityTest {
@Test
public void testPointEquality(Env env) {
Point p1 = new Pos(10.5, 42.0, -5.5);
Point p2 = new Pos(10.5, 42.0, -5.5);
// Assert points are equal
TestUtils.assertPoint(p1, p2);
}
@Test
public void testCollectionOrder(Env env) {
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("c", "b", "a");
// Assert collections have same elements, ignore order
TestUtils.assertEqualsIgnoreOrder(list1, list2);
}
@Test
public void testSNBT(Env env) {
CompoundBinaryTag nbt = CompoundBinaryTag.builder()
.putString("name", "test")
.putInt("value", 42)
.build();
// Assert NBT matches SNBT string
TestUtils.assertEqualsSNBT("{name:'test',value:42}", nbt);
}
@Test
public void testIgnoreSpaces(Env env) {
String s1 = "hello world";
String s2 = "hello world";
// Assert strings are equal ignoring extra spaces
TestUtils.assertEqualsIgnoreSpace(s1, s2);
}
}
Complete Example: Testing a Game Mode
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Instance;
import net.minestom.server.coordinate.Pos;
import net.minestom.testing.Collector;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class SurvivalGameModeTest {
@Test
public void testPlayerJoinGame(Env env) {
// Setup
Instance instance = env.createFlatInstance();
SurvivalGame game = new SurvivalGame(instance);
// Create player
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Player joins game
game.addPlayer(player);
env.tick();
// Verify player is in game
assertTrue(game.hasPlayer(player));
assertEquals(GameMode.SURVIVAL, player.getGameMode());
}
@Test
public void testPlayerRespawn(Env env) {
// Setup
Instance instance = env.createFlatInstance();
SurvivalGame game = new SurvivalGame(instance);
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
game.addPlayer(player);
// Track respawn event
Collector<PlayerRespawnEvent> events = env.trackEvent(
PlayerRespawnEvent.class,
EventFilter.PLAYER,
player
);
// Kill player
player.kill();
env.tick();
// Verify respawn
events.assertSingle();
assertTrue(player.isAlive());
}
@Test
public void testGameTimer(Env env) {
// Setup
Instance instance = env.createFlatInstance();
SurvivalGame game = new SurvivalGame(instance);
game.setDuration(Duration.ofSeconds(10));
// Start game
game.start();
// Tick until game ends
boolean ended = env.tickWhile(
() -> !game.isEnded(),
Duration.ofSeconds(15)
);
assertTrue(ended);
assertTrue(game.isEnded());
}
}
Best Practices
Test Isolation
Each test gets a fresh server instance. Don’t rely on state from other tests.
Clean Up
Destroy instances you create to prevent memory leaks in your test suite.
Use Collectors
Collectors provide powerful assertions for events and packets.
Tick Appropriately
Remember to tick the server after actions that need processing.
Common Testing Patterns
Testing Block Placement
@Test
public void testBlockPlacement(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Place block
instance.setBlock(5, 42, 5, Block.STONE);
env.tick();
// Verify block placement
assertEquals(Block.STONE, instance.getBlock(5, 42, 5));
}
Testing Commands
@Test
public void testCustomCommand(Env env) {
Instance instance = env.createFlatInstance();
Player player = env.createPlayer(instance, new Pos(0, 42, 0));
// Register command
CommandManager commandManager = env.process().command();
commandManager.register(new TeleportCommand());
// Execute command
commandManager.execute(player, "/teleport 10 50 10");
env.tick();
// Verify result
TestUtils.assertPoint(new Pos(10, 50, 10), player.getPosition());
}
Testing Async Operations
@Test
public void testAsyncChunkLoad(Env env) {
Instance instance = env.createEmptyInstance();
// Load chunk asynchronously
CompletableFuture<Chunk> future = instance.loadChunk(0, 0);
// Wait for completion
Chunk chunk = future.join();
assertNotNull(chunk);
assertTrue(chunk.isLoaded());
}
Integration with CI/CD
GitHub Actions Example
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Run tests
run: ./gradlew test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: build/test-results/
Next Steps
Performance
Learn how to benchmark your code
Extensions
Create testable modular extensions
