LightKeeper / Open-source Minecraft plugin testing

End-to-end testing for Minecraft plugins

Real servers. Not a mock.

Test your plugin against real Paper and Spigot servers. Spawn players, drive menus, inspect worlds, and catch regressions before your players do.

JAVA 21 / MAVEN 3.9+ / PAPER + SPIGOT
Linux & macOS recommended · Requirements

BlockPlacementIT.javaJava / JUnit
import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework;
import nl.pim16aap2.lightkeeper.framework.LightkeeperExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static nl.pim16aap2.lightkeeper.framework.assertions.LightkeeperAssertions.assertThat;

@ExtendWith(LightkeeperExtension.class)
class BlockPlacementIT
{
    @Test
    void placeBlock_shouldUpdateWorld(ILightkeeperFramework framework)
    {
        // setup
        var world = framework.worlds().main();
        var player = framework.bots().builder()
            .withName("lk_tester")
            .atSpawn(world)
            .build();

        // execute
        player.placeBlock("minecraft:stone", 1, 100, 0)
            .andWaitTicks(1);

        // verify
        assertThat(world).hasBlockAt(1, 100, 0)
            .ofType("minecraft:stone");
    }
}

Real Minecraft runtime

No mocked Bukkit APIs

Cached server builds

Made for repeated test runs

Fluent JUnit API

Test behavior, not internals

/ Get started in minutes

Your first passing test.

Set up the Maven plugin and JUnit framework, write a test, and run it against a real Paper or Spigot server.

Add the Maven dependencies

Start with this test project, or merge its configuration into your plugin’s POM. JitPack resolves both the test dependency and the build plugin. The Spigot API supplies Bukkit types used by assertions. Replace the example userAgent with your project and contact URL.

pom.xml · complete test project
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>my-plugin-tests</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <lightkeeper.version>master-SNAPSHOT</lightkeeper.version>
  </properties>

  <repositories>
    <repository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
    </repository>
    <repository>
      <id>spigotmc-snapshots</id>
      <url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
    </pluginRepository>
  </pluginRepositories>

  <dependencies>
    <dependency>
      <groupId>com.github.PimvanderLoos.LightKeeper</groupId>
      <artifactId>lightkeeper-framework-junit</artifactId>
      <version>${lightkeeper.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.spigotmc</groupId>
      <artifactId>spigot-api</artifactId>
      <version>1.21.11-R0.2-SNAPSHOT</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>com.github.PimvanderLoos.LightKeeper</groupId>
        <artifactId>lightkeeper-maven-plugin</artifactId>
        <version>${lightkeeper.version}</version>
        <executions>
          <execution>
            <id>prepare-server</id>
            <phase>pre-integration-test</phase>
            <goals><goal>prepare-server</goal></goals>
            <configuration>
              <serverType>paper</serverType>
              <userAgent>MyPlugin/1.0 (https://example.com/my-plugin)</userAgent>
              <runtimeManifestPath>${project.build.directory}/lightkeeper/runtime-manifest.json</runtimeManifestPath>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>3.5.6</version>
        <configuration>
          <systemPropertyVariables>
            <lightkeeper.runtimeManifestPath>${project.build.directory}/lightkeeper/runtime-manifest.json</lightkeeper.runtimeManifestPath>
          </systemPropertyVariables>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>integration-test</goal>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

These guides use master-SNAPSHOT. The Maven plugin belongs in build/plugins. See complete setup →

Write a test. Use a real world.

Save this as src/test/java/FirstPassingTestIT.java. The extension starts the prepared server and injects the framework. This block assertion needs no fixture plugin.

FirstPassingTestIT.java
import nl.pim16aap2.lightkeeper.framework.ILightkeeperFramework;
import nl.pim16aap2.lightkeeper.framework.LightkeeperExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static nl.pim16aap2.lightkeeper.framework.assertions.LightkeeperAssertions.assertThat;

@ExtendWith(LightkeeperExtension.class)
class FirstPassingTestIT
{
    @Test
    void placeBlock_shouldUpdateWorld(ILightkeeperFramework framework)
    {
        // setup
        var world = framework.worlds().main();
        var player = framework.bots().builder()
            .withName("lk_tester")
            .atSpawn(world)
            .build();

        // execute
        player.placeBlock("minecraft:stone", 1, 100, 0)
            .andWaitTicks(1);

        // verify
        assertThat(world).hasBlockAt(1, 100, 0)
            .ofType("minecraft:stone");
    }
}

To test your own plugin, configure its packaged JAR for provisioning. Explore what you can test →

Run it. Inspect the result.

With the POM from step 01 and an *IT.java test in place, run the full integration lifecycle.

Terminal
mvn verify
  1. pre-integration-test prepares a Paper server and writes the runtime manifest.
  2. Failsafe runs your test against that server, then verifies the result.
  3. Read test results in target/failsafe-reports/ and server logs under target/lightkeeper-server/.

Spigot is also supported; its first preparation builds the server with BuildTools. Cached builds are reused. Prefer the IDE gutter? Set it up →

/ What you can test

Everything that happens
inside the server.

From player interactions to a server restart, test how your plugin behaves in the environment that matters: a real Minecraft server.

01 / 03

Player interactions

Drive synthetic players through commands, chat, inventories, and menu clicks. Opt into the full-login pipeline when your test needs the real login path.

Explore player APIs
02 / 03

Worlds & events

Assert block states, inspect entities, capture Bukkit events, and wait for server-side outcomes with fluent assertions.

Explore worlds & events
03 / 03

Lifecycle & recovery

Restart or crash servers mid-test, provision plugins and worlds, and collect diagnostics when tests fail.

See how it works

/ Go deeper

The right docs for your build.

Human guides describe current master. The API Reference lets you choose from published API versions, including the master development build.

Browse API versions