Skip to content

v0.1.0 Writing Tests

github-actions[bot] edited this page Oct 8, 2026 · 5 revisions

⚠️ Documentation of Golem v0.1.0. The latest release is v0.7.0: this page in v0.7.0 · what changed · other versions

Where tests live

Golem loads every .php file in your tests folder (tests/ by default, see Configuration) and runs every concrete class extending Golem\TestCase. Helper classes and abstract base tests can live in the same folder.

Any namespace works. A convention that keeps IDEs happy is your plugin's namespace plus \Tests, declared in composer.json:

"autoload-dev": {
    "psr-4": { "MyName\\MyPlugin\\Tests\\": "tests/" }
}

Test methods

A test is a public, non-static method whose name starts with test, or any public method marked with #[Golem\Attribute\Test]. Tests run one after the other, in file order, on the server's main thread. Your plugin is already enabled and the default world is loaded.

public function testConfigHasDefaults(): void
{
    $this->assertSame(5, $this->plugin()->getConfig()->get('cooldown'));
}

Waiting: tests as generators

Most interesting behaviour takes time: a player joining, a delayed task, a cooldown. Declare the test as returning Generator and yield what you are waiting for. The test pauses, the server keeps ticking, and the test resumes when the value is ready.

Yield this To wait for You get back
$this->golem('Steve') a simulated player to join the Golem
$this->wait(40) 40 ticks (20 ticks = 1 second) null
$this->waitUntil(fn () => ..., 100) a condition, checked every tick the condition's value
any PocketMine Promise the promise to resolve its value
public function testRewardsAfterTenSeconds(): Generator
{
    $steve = yield $this->golem('Steve');

    yield $this->wait(10 * 20);

    $this->assertHasItem($steve, VanillaItems::DIAMOND());
}

public function testLoadsTheLeaderboard(): Generator
{
    $board = yield $this->waitUntil(
        fn () => $this->plugin()->getLeaderboard(),   // returns null until loaded
        timeoutTicks: 200,
        description: 'the leaderboard to load',
    );

    $this->assertCount(10, $board->getEntries());
}

waitUntil() throws Golem\WaitTimedOut when the condition is still falsy after the timeout, which fails the test and points at the yield that was waiting.

setUp and tearDown

setUp() runs before each test and may itself be a generator, which is handy to share golems:

final class ShopTest extends TestCase
{
    private Golem $buyer;

    protected function setUp(): Generator
    {
        $this->buyer = yield $this->golem('Buyer');
        $this->buyer->give(VanillaItems::EMERALD()->setCount(10));
    }

    public function testBuysASword(): void
    {
        $this->buyer->chat('/shop buy sword');

        $this->assertHasItem($this->buyer, VanillaItems::DIAMOND_SWORD());
    }
}

tearDown() runs after each test, even a failed one. After it, Golem disconnects every golem the test spawned and revokes their op status, so each test starts with an empty server.

Isolation

Golems are cleaned up between tests, but the world and your plugin's state are not: a block broken in one test stays broken in the next, and data your plugin keeps in memory survives. Give each test its own spot (teleport golems to different coordinates) or reset the state you rely on in setUp().

Attributes

use Golem\Attribute\Skip;
use Golem\Attribute\Test;
use Golem\Attribute\Timeout;

#[Timeout(ticks: 600)]                   // every test of the class may take 30 seconds
final class BossFightTest extends TestCase
{
    #[Test]                              // runs although the name does not start with "test"
    public function bossSpawnsAtNight(): Generator { /* ... */ }

    #[Skip('waiting for the 2.0 loot tables')]
    public function testLoot(): void { /* ... */ }
}

The default timeout is 200 ticks (10 seconds) per test. You can also skip from inside a test with $this->skip('reason').

Useful helpers

Method Returns
$this->server() the Server
$this->plugin() your plugin instance (or plugin('Other') for another one)
$this->world() the default world, a superflat world created fresh for each run
$this->spawn() the default world's spawn position

Clone this wiki locally