Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

keizah7's avatar
Level 17

Task Scheduling testing enviroment

I decided to make tests for task scheduling.

/** @test */
function it_runs_once_a_month()
{
    $this->travelTo(now()->startOfMonth());
    $this->artisan('schedule:run');
    $this->assertDatabaseCount((new HierarchyHistory)->getTable(), 0);

    $this->travelTo(now()->endOfMonth()->setTime(23, 00));
    $this->artisan('schedule:work');

    $this->assertDatabaseCount((new HierarchyHistory)->getTable(), $this->hierarchies->count());
}

I encountered one problem. Scheduled commands don't use testing enviroment variables if they called through scheduler. They using variables from .env. If I call command manually it is using config settings from phpunit.xml as it should.

How I can solve this problem?

testing variables:

<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
0 likes
9 replies
bugsysha's avatar

I don't test what I don't own. I have no reason to test the schedule:* commands. I only test the "action" classes I have in the commands.

So if I have something like:

class SendEmails extends Command
{
    protected $signature = 'mail:send {user}';
    protected $description = 'Send a marketing email to a user';

    public function handle(DripEmailer $drip)
    {
        $drip->send(User::query()->find($this->argument('user')));
    }
}

I usually rewrite it as:

class SendEmails extends Command
{
    protected $signature = 'mail:send {user}';
    protected $description = 'Send a marketing email to a user';

    public function handle(SendMarketingEmailToUser $action)
    {
		$action($this->argument('user')); // I usually create "action" classes as invokable so I have a hint when I open that class what is used for
    }
}

class SendMarketingEmailToUser
{
    public function __construct(public MyDripEmailer $drip) {}

	public function __invoke(int $userId): void
    {
		$this->drip->send(User::query()->find($userId));
    }
}

Then I write two tests. One to test my SendMarketingEmailToUser class and the second one to test that the SendEmails command is calling SendMarketingEmailToUser class where I mock SendMarketingEmailToUser class.

Also, I never call DripEmailer directly. I either encapsulate it in some service class, or I just extend it and use MyDripEmailer class instead. That way if I have to change something I have a nice place where to write additional logic.

1 like
keizah7's avatar
Level 17

I have separate test for command itself, I wanted to test if scheduler works as need

bugsysha's avatar

The scheduler is something that is owned by the Laravel framework. They have their own tests for it. And if it was broken by any chance, considering how many developers and companies use Laravel, that would be spotted and fixed straight away. So in my book, there is no real reason to doubt or test it.

keizah7's avatar
Level 17

I understand that. I wanted to test if I set frequency options correctly not scheduler functionality itself

$schedule->command('save:hierarchy')
    ->dailyAt('23:00')
    ->when(fn () => Carbon::now()->endOfMonth()->isToday());
bugsysha's avatar

To me, it doesn't make sense for the corn job not to have a reference when it is scheduled at.

$schedule->command(SaveHierarchy::class)
    ->{SaveHierarchy::scheduleInterval()}(SaveHierarchy::scheduleTime())
    ->when(fn () => SaveHierarchy::scheduleWhen());

Then I can test those static methods to make sure I have correct values returned from them. A bunch of people tell me it is ugly, but I find it very useful so when I need to make a change I change that command class and not Kernel class.

I also use the following to define the same thing:

class Kernel extends ConsoleKernel
{
    protected $commands = [
		Commands\DoSomething::class,
        Commands\SaveHierarchy::class,
    ];

    protected function schedule(Schedule $schedule)
    {
		Commands\DoSomething::schedule($schedule);
		Commands\SaveHierarchy::schedule($schedule);
    }
}

Then in that class, I do the same as above:

// SaveHierarchy class
public static function schedule(Schedule $schedule): void
{
    $schedule->command(self::class)
        ->{self::scheduleInterval()}(self::scheduleTime())
        ->when(fn () => self::scheduleWhen());
}
keizah7's avatar
Level 17

Yeah, it looks kinda ugly :D You declare scheduler in console class, but still cant test if it schedules correctly, or you testing scheduleInterval and trusting schedule functionality of laravel itself?

bugsysha's avatar

@keizah you can test what you've set because you can do something like:

$this->assertSame('dailyAt', SaveHierarchy::scheduleInterval());
$this->assertSame('23:00', SaveHierarchy::scheduleTime());
// use Carbon::setTestNow(); to make this work
$this->assertTrue(Carbon::now()->endOfMonth()->isToday());
keizah7's avatar
Level 17

Yeah, but it doesn't guarantee that scheduler command will lauched once a month :/

bugsysha's avatar

@keizah try something like:

$schedule = resolve(Schedule::class);
$events = $schedule->events();

foreach ($events as $event) {
	// do the assertions
	// $event->getExpression() should contain "0 23 * * *" or similar
}

Please or to participate in this conversation.