Skip to main content
Testing is crucial for ensuring your plugin works correctly. Paper includes a test-plugin module that demonstrates testing patterns and plugin features.

Test Plugin Structure

Paper’s test plugin is located at test-plugin/ in the Paper repository and serves as both a testing tool and example implementation.

Test Plugin Components

From the Paper test plugin source:
name: Paper-Test-Plugin
version: ${version}
main: io.papermc.testplugin.TestPlugin
description: Paper Test Plugin
author: PaperMC
api-version: ${apiversion}
load: STARTUP
bootstrapper: io.papermc.testplugin.TestPluginBootstrap
loader: io.papermc.testplugin.TestPluginLoader
defaultPerm: FALSE
permissions:
dependencies:

Manual Testing

Manual testing involves running your plugin on a Paper server and testing functionality by hand.
1

Set up test server

Create a test server directory:
mkdir test-server
cd test-server

# Download Paper JAR
wget https://api.papermc.io/v2/projects/paper/versions/1.21.11/builds/latest/downloads/paper-1.21.11-latest.jar

# Accept EULA
echo "eula=true" > eula.txt

# Create plugins directory
mkdir plugins
2

Build and deploy plugin

Build your plugin and copy it to the test server:
mvn clean package
cp target/MyPlugin-1.0.0.jar test-server/plugins/
3

Run the server

Start the test server:
java -jar paper-1.21.11-latest.jar --nogui
Watch the console for:
  • Plugin loading messages
  • Any errors or warnings
  • Successful enable confirmation
4

Test functionality

Connect to the server and test:
  • Commands work correctly
  • Events fire as expected
  • Configuration loads properly
  • Permissions function correctly

Automated Testing Setup

While Paper doesn’t include a built-in testing framework, you can use standard Java testing tools.

Add Testing Dependencies

<dependencies>
    <!-- Paper API -->
    <dependency>
        <groupId>io.papermc.paper</groupId>
        <artifactId>paper-api</artifactId>
        <version>1.21.11-R0.1-SNAPSHOT</version>
        <scope>provided</scope>
    </dependency>
    
    <!-- JUnit for testing -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito for mocking -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.5.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Unit Testing

Test individual components in isolation:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.bukkit.entity.Player;
import org.bukkit.command.CommandSender;
import io.papermc.paper.command.brigadier.CommandSourceStack;

class HelloCommandTest {
    
    private HelloCommand command;
    private CommandSourceStack stack;
    private CommandSender sender;
    
    @BeforeEach
    void setUp() {
        command = new HelloCommand();
        stack = mock(CommandSourceStack.class);
        sender = mock(CommandSender.class);
        
        when(stack.getSender()).thenReturn(sender);
    }
    
    @Test
    void testExecuteWithNoArgs() {
        command.execute(stack, new String[]{});
        
        verify(sender).sendMessage("Hello, world!");
    }
    
    @Test
    void testExecuteWithName() {
        command.execute(stack, new String[]{"Steve"});
        
        verify(sender).sendMessage("Hello, Steve!");
    }
    
    @Test
    void testPermission() {
        assertEquals("myplugin.hello", command.permission());
    }
}

Testing Event Listeners

import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.entity.Player;
import net.kyori.adventure.text.Component;

class PlayerListenerTest {
    
    private PlayerListener listener;
    private PlayerJoinEvent event;
    private Player player;
    
    @BeforeEach
    void setUp() {
        listener = new PlayerListener();
        event = mock(PlayerJoinEvent.class);
        player = mock(Player.class);
        
        when(event.getPlayer()).thenReturn(player);
    }
    
    @Test
    void testPlayerJoinSendsWelcomeMessage() {
        listener.onPlayerJoin(event);
        
        verify(player).sendMessage("Welcome to the server!");
    }
    
    @Test
    void testJoinMessageModified() {
        listener.onPlayerJoin(event);
        
        verify(event).joinMessage(any(Component.class));
    }
}

Testing Configuration

import org.bukkit.configuration.file.YamlConfiguration;
import java.io.StringReader;

class ConfigTest {
    
    @Test
    void testDefaultConfiguration() {
        String yaml = """
            enable-feature: true
            max-players: 100
            welcome-message: Welcome!
            """;
        
        YamlConfiguration config = YamlConfiguration.loadConfiguration(
            new StringReader(yaml));
        
        assertTrue(config.getBoolean("enable-feature"));
        assertEquals(100, config.getInt("max-players"));
        assertEquals("Welcome!", config.getString("welcome-message"));
    }
    
    @Test
    void testConfigWrapper() {
        JavaPlugin plugin = mock(JavaPlugin.class);
        FileConfiguration fileConfig = new YamlConfiguration();
        fileConfig.set("enable-feature", true);
        fileConfig.set("max-players", 100);
        
        when(plugin.getConfig()).thenReturn(fileConfig);
        
        Config config = new Config(plugin);
        
        assertTrue(config.isFeatureEnabled());
        assertEquals(100, config.getMaxPlayers());
    }
}

Integration Testing

Test multiple components working together:
class PluginIntegrationTest {
    
    private JavaPlugin plugin;
    private Server server;
    
    @BeforeEach
    void setUp() {
        server = mock(Server.class);
        plugin = mock(JavaPlugin.class);
        
        when(plugin.getServer()).thenReturn(server);
        when(plugin.getDataFolder()).thenReturn(new File("test-data"));
    }
    
    @Test
    void testPluginInitialization() {
        // Test that plugin initializes correctly
        assertNotNull(plugin.getServer());
        assertNotNull(plugin.getDataFolder());
    }
}

Testing Best Practices

1. Mock External Dependencies

Server server = mock(Server.class);
Player player = mock(Player.class);
PluginManager pluginManager = mock(PluginManager.class);

when(server.getPluginManager()).thenReturn(pluginManager);
when(player.getName()).thenReturn("TestPlayer");

2. Test Edge Cases

@Test
void testCommandWithEmptyArgs() {
    command.execute(stack, new String[]{});
    // Verify appropriate handling
}

@Test
void testCommandWithNullArgs() {
    assertThrows(NullPointerException.class, () -> {
        command.execute(stack, null);
    });
}

@Test
void testCommandWithInvalidArgs() {
    command.execute(stack, new String[]{"invalid"});
    verify(sender).sendMessage(contains("Invalid"));
}

3. Use Descriptive Test Names

@Test
void playerWithoutPermissionCannotUseAdminCommand() {
    when(sender.hasPermission("myplugin.admin")).thenReturn(false);
    
    assertFalse(command.canUse(sender));
}

@Test
void configReloadPreservesCustomValues() {
    config.set("custom-value", "test");
    config.reload();
    
    assertEquals("test", config.getString("custom-value"));
}

4. Clean Up After Tests

@AfterEach
void tearDown() {
    // Clean up test files
    File testData = new File("test-data");
    if (testData.exists()) {
        deleteDirectory(testData);
    }
}

private void deleteDirectory(File dir) {
    File[] files = dir.listFiles();
    if (files != null) {
        for (File file : files) {
            if (file.isDirectory()) {
                deleteDirectory(file);
            } else {
                file.delete();
            }
        }
    }
    dir.delete();
}

Running Tests

# Run all tests
mvn test

# Run specific test class
mvn test -Dtest=HelloCommandTest

# Run with coverage
mvn test jacoco:report

Debugging

When debugging your plugin:
1

Enable debug logging

@Override
public void onEnable() {
    getLogger().setLevel(Level.FINE);
    getLogger().fine("Debug logging enabled");
}
2

Add strategic logging

@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
    getLogger().info("Player joining: " + event.getPlayer().getName());
    // Event handling logic
    getLogger().info("Join handling complete");
}
3

Use IDE debugger

Attach your IDE debugger to the running Paper server:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \
     -jar paper-1.21.11-latest.jar
Then connect your IDE to port 5005.
The Paper test-plugin module serves as an excellent reference for implementing various plugin features. Examine it at test-plugin/ in the Paper repository.

Continuous Integration

Automate testing with CI/CD:
.github/workflows/test.yml
name: Test Plugin

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: mvn test
      
      - name: Build plugin
        run: mvn package
Always test your plugin thoroughly before deploying to a production server. Use a dedicated test server to avoid disrupting players.

Build docs developers (and LLMs) love