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:
paper-plugin.yml
TestPlugin.java
TestPluginBootstrap.java
TestPluginLoader.java
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.
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
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/
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
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
Maven (pom.xml)
Gradle (build.gradle.kts)
< 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:
Enable debug logging
@ Override
public void onEnable () {
getLogger (). setLevel ( Level . FINE );
getLogger (). fine ( "Debug logging enabled" );
}
Add strategic logging
@ EventHandler
public void onPlayerJoin ( PlayerJoinEvent event) {
getLogger (). info ( "Player joining: " + event . getPlayer (). getName ());
// Event handling logic
getLogger (). info ( "Join handling complete" );
}
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.