Testing

Setup

The scheduler package includes mocked WordPress functions in tests/bootstrap.php. These mocks cover:

  • add_action() / add_filter() / do_action() / apply_filters()
  • get_transient() / set_transient() / delete_transient()
  • wp_next_scheduled() / wp_schedule_event() / wp_unschedule_event()
  • current_time() / is_multisite() / wp_doing_cron()

Running Tests

# Run all tests
composer test

# Run with verbose output
vendor/bin/phpunit --verbose

# Run a specific test class
vendor/bin/phpunit tests/Unit/ScheduleTest.php

# Run a specific test method
vendor/bin/phpunit --filter testCallReturnsScheduledTask

Testing Schedule Registration

use PHPUnit\Framework\TestCase;
use WPZylos\Framework\Scheduler\Schedule;
use WPZylos\Framework\Scheduler\ScheduledTask;

class MySchedulerTest extends TestCase
{
    public function testTasksAreRegistered(): void
    {
        $schedule = new Schedule();

        $schedule->call(function () {})->daily()->name('cleanup');
        $schedule->call(function () {})->hourly()->name('sync');

        $this->assertCount(2, $schedule->getTasks());
    }

    public function testCallbackTaskExecutes(): void
    {
        $schedule = new Schedule();
        $executed = false;

        $schedule->call(function () use (&$executed) {
            $executed = true;
        })->everyMinute()->name('test-task');

        $schedule->run();

        $this->assertTrue($executed);
    }
}

Testing Frequency Configuration

public function testDailyAtSetsCorrectTime(): void
{
    $task = new ScheduledTask(function () {}, 'callback');
    $task->dailyAt('14:30');

    $this->assertSame(86400, $task->getIntervalSeconds());
}

public function testWeeklyOnSetsCorrectDayAndTime(): void
{
    $task = new ScheduledTask(function () {}, 'callback');
    $task->weeklyOn(3, '10:00');

    $this->assertSame(604800, $task->getIntervalSeconds());
}

Testing Constraints

public function testWhenConstraintBlocksExecution(): void
{
    $task = new ScheduledTask(function () {}, 'callback');
    $task->everyMinute()->when(fn() => false);

    $this->assertFalse($task->isDue());
}

public function testSkipConstraintBlocksExecution(): void
{
    $task = new ScheduledTask(function () {}, 'callback');
    $task->everyMinute()->skip(fn() => true);

    $this->assertFalse($task->isDue());
}

Testing Overlap Prevention

public function testOverlapPreventionBlocksExecution(): void
{
    $GLOBALS['wp_transients'] = [];
    $counter = 0;

    $task = new ScheduledTask(function () use (&$counter) {
        $counter++;
    }, 'callback');

    $task->everyMinute()->name('lock-test')->withoutOverlapping();

    // Simulate existing lock
    $lockKey = 'scheduler_lock_' . md5('lock-test');
    set_transient($lockKey, time(), 3600);

    $task->run();

    $this->assertSame(0, $counter);
}

public function testOverlapLockIsReleasedAfterExecution(): void
{
    $GLOBALS['wp_transients'] = [];

    $task = new ScheduledTask(function () {
        // Task completes
    }, 'callback');

    $task->everyMinute()->name('release-test')->withoutOverlapping();
    $task->run();

    $lockKey = 'scheduler_lock_' . md5('release-test');
    $this->assertFalse(get_transient($lockKey));
}

Testing Error Handling

public function testFailedTaskFiresAction(): void
{
    $GLOBALS['fired_actions'] = [];

    $schedule = new Schedule();
    $schedule->call(function () {
        throw new \RuntimeException('Test failure');
    })->everyMinute()->name('failing-task');

    $schedule->run();

    $this->assertArrayHasKey('wpzylos_scheduler_task_failed', $GLOBALS['fired_actions']);
}

Resetting State Between Tests

Always reset global state in setUp():

protected function setUp(): void
{
    parent::setUp();

    $GLOBALS['wp_transients'] = [];
    $GLOBALS['fired_actions'] = [];
    $GLOBALS['wp_scheduled_events'] = [];
}

Code Quality

# Static analysis
composer analyze

# Code style check
composer cs

# Fix code style
composer cs-fix

# Run everything
composer qa