Skip to main content

Overview

Skript includes a comprehensive testing framework that supports both script-based tests and JUnit tests. This allows you to verify that your custom syntax elements work correctly.

JUnit Testing

Skript provides base classes for writing JUnit tests that can interact with Bukkit and Skript.

SkriptJUnitTest Base Class

public abstract class SkriptJUnitTest {
    
    /**
     * @return the test world.
     */
    public static World getTestWorld() {
        return Bukkit.getWorlds().get(0);
    }
    
    /**
     * @return the testing location at the spawn of the testing world.
     */
    public static Location getTestLocation() {
        return getTestWorld().getSpawnLocation().add(0, 1, 0);
    }
    
    /**
     * Spawns a testing pig at the spawn location of the testing world.
     * @return Pig that has been spawned.
     */
    public static Pig spawnTestPig() {
        return spawnTestEntity(EntityType.PIG);
    }
    
    /**
     * Spawns a test Entity from the provided entityType
     * @param entityType The desired EntityType to spawn
     * @return The spawned Entity
     */
    public static <E extends Entity> E spawnTestEntity(EntityType entityType) {
        if (delay <= 0D)
            delay = 1; // A single tick allows the entity to spawn
        return (E) getTestWorld().spawnEntity(getTestLocation(), entityType);
    }
    
    /**
     * Set the type of the block at the testing location.
     * @param material The material to set the block to.
     * @return the Block after it has been updated.
     */
    public static Block setBlock(Material material) {
        Block block = getBlock();
        block.setType(material);
        return block;
    }
    
    /**
     * Return the main block for testing in the getTestLocation();
     * @return the Block after it has been updated.
     */
    public static Block getBlock() {
        return getTestWorld().getSpawnLocation().add(10, 1, 0).getBlock();
    }
}
Source: /src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java:30-116

Test World Setup

The test world is automatically configured with appropriate game rules:
static {
    World world = getTestWorld();
    world.setGameRule(GameRule.MAX_ENTITY_CRAMMING, 1000);
    world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
    // Natural entity spawning
    world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
    world.setGameRule(GameRule.MOB_GRIEFING, false);
    
    if (Skript.isRunningMinecraft(1, 15)) {
        world.setGameRule(GameRule.DO_PATROL_SPAWNING, false);
        world.setGameRule(GameRule.DO_TRADER_SPAWNING, false);
        world.setGameRule(GameRule.DISABLE_RAIDS, false);
    }
}
Source: /src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java:15-28

Cleanup After Tests

/**
 * Override this method if your JUnit test requires block modification 
 * with delay over 1 tick.
 */
public void cleanup() {
    getTestWorld().getEntities().forEach(Entity::remove);
    setBlock(Material.AIR);
}
Source: /src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java:56-60

Setting Test Delays

If your test needs time to complete:
/**
 * The delay this JUnit test is requiring to run.
 * Do note this is global to all other tests. The most delay is the final waiting time.
 * @return the delay in Minecraft ticks this junit test is requiring to run for.
 */
public static long getShutdownDelay() {
    return delay;
}

/**
 * @param delay Set the delay in Minecraft ticks for this test to run.
 */
public static void setShutdownDelay(long delay) {
    SkriptJUnitTest.delay = delay;
}
Source: /src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java:38-52

Writing a JUnit Test

Basic Test Structure

import ch.njol.skript.test.runner.SkriptJUnitTest;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.junit.Test;
import static org.junit.Assert.*;

public class MySkriptTest extends SkriptJUnitTest {
    
    @Test
    public void testBlockSetting() {
        // Set up test
        Block block = setBlock(Material.STONE);
        
        // Verify
        assertEquals(Material.STONE, block.getType());
        
        // Cleanup happens automatically via cleanup() method
    }
    
    @Test
    public void testEntitySpawning() {
        // Spawn a test pig
        Pig pig = spawnTestPig();
        
        // Verify it spawned
        assertNotNull(pig);
        assertTrue(pig.isValid());
        assertEquals(getTestLocation().getWorld(), pig.getWorld());
    }
}

Tests Requiring Delays

public class DelayedTest extends SkriptJUnitTest {
    
    @Test
    public void testWithDelay() {
        // Set delay to 20 ticks (1 second)
        setShutdownDelay(20);
        
        // Spawn entity that needs time to process
        Pig pig = spawnTestPig();
        
        // Schedule verification for later
        Bukkit.getScheduler().runTaskLater(plugin, () -> {
            assertTrue(pig.isValid());
        }, 10);
    }
    
    @Override
    public void cleanup() {
        // Custom cleanup that runs after the delay
        getTestWorld().getEntities().forEach(Entity::remove);
    }
}

Test Execution Flow

Skript’s test runner executes tests with this flow:
private void runTest(Class<?> clazz, AtomicLong shutdownDelay, 
        AtomicLong tests, AtomicLong milliseconds, 
        AtomicLong ignored, AtomicLong fails)
        throws NoSuchMethodException, InvocationTargetException, 
        InstantiationException, IllegalAccessException {
    String test = clazz.getName();
    SkriptJUnitTest.setCurrentJUnitTest(test);
    SkriptJUnitTest.setShutdownDelay(0);
    
    info("Running JUnit test '" + test + "'");
    Result result = JUnitCore.runClasses(clazz);
    TestTracker.testStarted("JUnit: '" + test + "'");
    
    shutdownDelay.set(Math.max(shutdownDelay.get(), 
        SkriptJUnitTest.getShutdownDelay()));
    tests.getAndAdd(result.getRunCount());
    milliseconds.getAndAdd(result.getRunTime());
    ignored.getAndAdd(result.getIgnoreCount());
    fails.getAndAdd(result.getFailureCount());
    
    // If JUnit failures are present, add them to the TestTracker.
    for (Failure failure : result.getFailures()) {
        String message = failure.getMessage() == null ? 
            "" : " " + failure.getMessage();
        TestTracker.JUnitTestFailed(test, message);
        Skript.exception(failure.getException(), 
            "JUnit test '" + failure.getTestHeader() + " failed.");
    }
    
    if (SkriptJUnitTest.class.isAssignableFrom(clazz) &&
        !SkriptAsyncJUnitTest.class.isAssignableFrom(clazz))
        ((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup();
    SkriptJUnitTest.clearJUnitTest();
}
Source: /src/main/java/ch/njol/skript/Skript.java:994-1039

Script-Based Testing

Skript also supports writing tests directly in Skript syntax.

Test Case Event

@NoDoc
public class EvtTestCase extends SkriptEvent {
    
    static {
        if (TestMode.ENABLED && !TestMode.GEN_DOCS) {
            Skript.registerEvent("Test Case", EvtTestCase.class, 
                SkriptTestEvent.class, "test %string% [when <.+>]")
                .description("Contents represent one test case.")
                .since("2.5");
        }
    }
    
    private Literal<String> name;
    @Nullable
    private Condition condition;
    
    @Override
    public boolean check(Event event) {
        String n = name.getSingle();
        if (n == null)
            return false;
        Skript.info("Running test case " + n);
        TestTracker.testStarted(n);
        return true;
    }
}
Source: /src/main/java/ch/njol/skript/test/runner/EvtTestCase.java:14-52

Example Test Script

test "broadcast works":
    broadcast "test message"
    # Verify broadcast happened

test "player has correct name":
    set {_name} to name of player
    assert {_name} is set with "Player name should exist"

Test Mode Configuration

Tests run in specific modes controlled by system properties:
public class TestMode {
    public static final boolean ENABLED = 
        System.getProperty("skript.tests") != null;
    
    public static final boolean DEV_MODE = 
        "true".equalsIgnoreCase(System.getProperty("skript.tests.dev"));
    
    public static final boolean JUNIT = 
        "true".equalsIgnoreCase(System.getProperty("skript.tests.junit"));
    
    public static final boolean GEN_DOCS = 
        "true".equalsIgnoreCase(System.getProperty("skript.tests.gendocs"));
}

Running Tests

Skript runs all tests during the test phase:
info("Loading all tests from " + TestMode.TEST_DIR);

// Treat parse errors as fatal testing failure
TestingLogHandler errorCounter = new TestingLogHandler(Level.SEVERE);
try {
    errorCounter.start();
    
    // load test directory scripts
    ScriptLoader.loadScripts(TestMode.TEST_DIR.toFile(), errorCounter);
} finally {
    errorCounter.stop();
}

Bukkit.getPluginManager().callEvent(new SkriptTestEvent());
if (errorCounter.getCount() > 0) {
    TestTracker.testStarted("parse scripts");
    TestTracker.testFailed(errorCounter.getCount() + " error(s) found");
}
Source: /src/main/java/ch/njol/skript/Skript.java:880-902

Best Practices

Always implement the cleanup() method to remove entities and reset blocks.
Set shutdownDelay when your test needs time for async operations to complete.
Write tests for null values, empty inputs, and boundary conditions.
Each test should be independent and not rely on other tests’ state.

Example: Complete Test Suite

import ch.njol.skript.test.runner.SkriptJUnitTest;
import org.bukkit.Material;
import org.bukkit.entity.Pig;
import org.junit.Test;
import org.junit.After;
import static org.junit.Assert.*;

public class MyAddonTest extends SkriptJUnitTest {
    
    @Test
    public void testCustomExpression() {
        // Test your custom expression
        Pig pig = spawnTestPig();
        
        // Verify the expression works
        assertNotNull(pig);
        assertTrue(pig.hasAI());
    }
    
    @Test
    public void testCustomEffect() {
        // Test your custom effect
        Block block = setBlock(Material.STONE);
        
        assertEquals(Material.STONE, block.getType());
    }
    
    @Test
    public void testWithMinecraftTicks() {
        // This test needs 40 ticks to complete
        setShutdownDelay(40);
        
        Pig pig = spawnTestPig();
        
        // Schedule a check for later
        Bukkit.getScheduler().runTaskLater(plugin, () -> {
            assertTrue(pig.isValid());
        }, 20);
    }
    
    @Override
    public void cleanup() {
        // Clean up all test entities and blocks
        super.cleanup();
        
        // Additional custom cleanup
        getTestWorld().getEntities().stream()
            .filter(e -> e instanceof Pig)
            .forEach(Entity::remove);
    }
}

Next Steps

Syntax Registration

Learn more about registering custom syntax

Addon Development

Back to addon development overview

Build docs developers (and LLMs) love