25

Environment

  • Laravel Version : 5.1.45 (LTS)

  • PHP Version : 5.6.1


Description

I'm trying to run a command every 1 minute using Laravel Task Scheduling.


Attempt

I've added this line to my cron tab file

* * * * * php artisan schedule:run >> /dev/null 2>&1

Here is my /app/Console/Kernel.php

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        \App\Console\Commands\Inspire::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('inspire')->hourly();
        $schedule->command('echo "Happy New Year!" ')->everyMinute(); //<---- ADD HERE        }
}

I've added this line $schedule->command('echo "Happy New Year!" ')->everyMinute();


Question

How do I test this ?

How do I trigger my echo to display ?

How do I know if what I did is not wrong ?

1
  • 1
    What I have been doing is having each "command" be actually a runnable artisan command. Then u can test that command by itself to ensure it does what you want it to. I don't bother to test that the scheduler works as expected bc it's provided by the framework so I can assume it works. The only thing left vulnerable is if you made a typo or incorrectly set up the scheduler but if you follow the docs there's no reason why it shouldn't work and it would sort of just be testing bc someone told u to test everything but the value is minimal.. feel free to disagree Commented Oct 20, 2016 at 18:57

7 Answers 7

24

command() runs an artisan command. What you're trying to achieve - issuing a command to the OS - is done by exec('echo "Happy New Year!"')

Testing depends on what you want to test:

  • Whether the scheduler (every minute) is working?

In this case, you don't have to. It is tested in the original framework code.

  • Whether the command succeeds?

Well, you can manually run php artisan schedule:run and see the output.

The scheduler does not produce any output on default (>> /dev/null 2>&1). You can, however, redirect the output of the runned scripts to any file by chaining writeOutputTo() or appendOutputTo() (https://laravel.com/docs/5.1/scheduling#task-output).


For more complex logic, write a console command instead (https://laravel.com/docs/5.1/artisan#writing-commands) and use command() - this way you can write nice, testable code.

Sign up to request clarification or add additional context in comments.

4 Comments

I've update my line $schedule->command('curl http://wttr.in/01851')->everyMinute()->sendOutputTo(public_path().'/tasks/log.txt'); and get my log to export, but in my log.txt, I open it up, I see [InvalidArgumentException] Command "curl" is not defined. what did I possibly do wrong ?
@ihue The command function runs artisan commands, not terminal commands to the OS
As I wrote above, use $schedule->exec() instead of command() :)
@tam : Sorry guys for the confusion.
14

If you want to unit test the scheduling of events you can use this example. It is based on the default inspire command:

public function testIsAvailableInTheScheduler()
{
    /** @var \Illuminate\Console\Scheduling\Schedule $schedule */
    $schedule = app()->make(\Illuminate\Console\Scheduling\Schedule::class);

    $events = collect($schedule->events())->filter(function (\Illuminate\Console\Scheduling\Event $event) {
        return stripos($event->command, 'YourCommandHere');
    });

    if ($events->count() == 0) {
        $this->fail('No events found');
    }

    $events->each(function (\Illuminate\Console\Scheduling\Event $event) {
        // This example is for hourly commands.
        $this->assertEquals('0 * * * * *', $event->expression);
    });
}

1 Comment

This is a nice approach, but might not work in L8, an $event->description might be required. See my answer below.
11

I've had luck in Laravel 8 with the following test:

public function testRunsAt930()
{
    Event::fake();
    $this->travelTo(now()->startOfWeek()->setHour(9)->setMinute(30));
    $this->artisan('schedule:run');
    
    Event::assertDispatched(ScheduledTaskFinished::class, function ($event) {
        return strpos($event->task->command, 'your-command-name') !== false;
    });
}

The scheduler fires off the Illuminate\Console\Events\ScheduledTaskFinished event when it successfully runs a schedule, so you can find out if your schedule would run at a particular time by mocking your time then actually running the schedule and then you just listen for that event.

If you're not using L8 you can probably use something like Carbonite for the time mocking, though I think the rest should work, though you might need to use Artisan facade in earlier versions.

To find out if a schedule ran just use Event::assertDispatched() as in the above example. Though if you want to find out if it did not run you can just use Event::assertNotDispatched() for the ScheduledTaskFinished::class or listen for the ScheduledTaskFailed::class instead.

I know this has already been answered but I found this method may be a bit cleaner if you're using L8, and maybe even for earlier versions.

3 Comments

Note scheduled commands will be executed though: see Schedule::exec() and ScheduleRunCommand::handle() into $this->runEvent() into $event->run().
That is not the case for scheduled jobs when you'd use \Bus::fake() or \Queue::fake() though.
This solution works on Laravel 6.0, while other solution doesn't work since $schedule->events() weirdly return empty to me.
5

Building on Michiel's answer, I've used the methods contained in Illuminate\Console\Scheduling\Event to test if the event is due to run for a given date.

I've mocked the current date using Carbon::setTestNow() so that any date based logic in the when() and skip() filters will behave as expected.

use Tests\TestCase;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Console\Scheduling\Event;

use Cron\CronExpression;
use Carbon\Carbon;


class ScheduleTest extends TestCase {


    public function testCompanyFeedbackSchedule()
    {
        $event = $this->getCommandEvent('your-command-signature');

        $test_date = Carbon::now()->startOfDay()->addHours(8);

        for ($i=0; $i < 365; $i++) { 
            $test_date->addDay();
            Carbon::setTestNow($test_date);

            // Run the when() & skip() filters
            $filters_pass = $event->filtersPass($this->app);
            // Test that the Cron expression passes
            $date_passes = $this->isEventDue($event);
            $will_run = $filters_pass && $date_passes;

            // Should only run on first friday of month
            if ($test_date->format('l') === 'Friday' && $test_date->weekOfMonth === 1) {
                $this->assertTrue($will_run, 'Task should run on '. $test_date->toDateTimeString());
            } else {
                $this->assertFalse($will_run, 'Task should not run on '. $test_date->toDateTimeString());
            }
        }
    }


    /**
     * Get the event matching the given command signature from the scheduler
     * 
     * @param  string  $command_signature
     * 
     * @return Illuminate\Console\Scheduling\Event
     */
    private function getCommandEvent($command_signature)
    {
        $schedule = app()->make(Schedule::class);

        $event = collect($schedule->events())->first(function (Event $event) use ($command_signature) {
            return stripos($event->command, $command_signature);
        });

        if (!$event) {
            $this->fail('Event for '. $command_signature .' not found');
        }

        return $event;
    }


    /**
     * Determine if the Cron expression passes.
     * 
     * Copied from the protected method Illuminate\Console\Scheduling\Event@isEventDue
     * 
     * @return bool
     */
    private function isEventDue(Event $event)
    {
        $date = Carbon::now();

        if ($event->timezone) {
            $date->setTimezone($event->timezone);
        }

        return CronExpression::factory($event->expression)->isDue($date->toDateTimeString());
    }
}

Comments

3

Building on Michiel's anwer as well, at least in L8 there is a difference.

Instead of $event->command:

$events = collect($schedule->events())->filter(function (Event $event) {
  return stripos($event->command, 'YourCommandHere');
});

An $event->description is required:

$events = collect($schedule->events())->filter(function (Event $event) {
  return stripos($event->description, 'YourCommandHere');
});

I have found this while looking into Tinker:

>>> app()->make(\Illuminate\Console\Scheduling\Schedule::class)->events();
=> [
     Illuminate\Console\Scheduling\CallbackEvent {#3496
       +command: null,
       +expression: "* * * * *",
       +timezone: "UTC",
       +user: null,
       +environments: [],
       +evenInMaintenanceMode: false,
       +withoutOverlapping: false,
       +onOneServer: false,
       +expiresAt: 1440,
       +runInBackground: false,
       +output: "/dev/null",
       +shouldAppendOutput: false,
       +description: "App\Jobs\GenerateSuggestion",
       +mutex: Illuminate\Console\Scheduling\CacheEventMutex {#3498
         +cache: Illuminate\Cache\CacheManager {#282},
         +store: null,
       },
       +exitCode: null,
     },
   ]

Comments

2

You can check if job added to scheduler (L9) :

$addedToScheduler = collect(App::make(Schedule::class)->events())
   ->pluck('description')
   ->contains(PaymentRenewJob::class);

$this->assertTrue($addedToScheduler, 'PaymentRenewJob is not added to scheduler');

Rest of your code can tested on console testing. see more on laravel docs eg:

$this->artisan('inspire')->assertSuccessful();

Comments

0

This solution is inspired by Hasan's answer.

<?php
    
    namespace App\Console\Callable;
    
    class ExampleCallable
    {
        public function __invoke(): void
        {
            // @todo Do something
        }
    }

The main difference is that scheduled item is just a callable class and has an empty description value. Collection is filtered using reflection to access the protected callback parameter to filter out unsuitable events and then check if there are any events with suitable execution schedule. Detection approach is taken from the core schedule list command.

<?php

namespace Tests\Unit\Console;

use App\Console\Callable\ExampleCallable;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\App;
use ReflectionClass;
use Tests\TestCase;

class KernelTest extends TestCase
{
    public function testSchedule(): void
    {
        // Collect all scheduled events and filter out suitable callbacks
        $events = collect(App::make(Schedule::class)->events())
            ->filter(function ($event) {
                $callback = (new ReflectionClass($event))
                    ->getProperty('callback')
                    ->getValue($event);

                return $callback instanceof ExampleCallable;
            });

        $this->assertFalse($events->isEmpty());
        $this->assertNotEquals(0,
            $events
                ->where('expression', '* * * * *')
                ->count()
        );
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.