laraveltestingphpunitlaravel-jobs

How to test a scheduled job in laravel


I have one issue with testing and I hope you can help me.

I have a scheduled job that collects information about others websites. it is a web crawler. This is the job:

<?php

namespace App\Jobs;

use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class FetchPostsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $baseUrl = 'https://post.com';                         

    public function handle()
    {                  
        ini_set('max_execution_time', 180);
        $response = Http::get($this->baseUrl.'/posts');        
        $htmlString = $response->body();               
        //add this line to suppress any warnings
        libxml_use_internal_errors(true);
        $doc = new \DOMDocument();
        $doc->loadHTML($htmlString);
        $xpath = new \DOMXPath($doc);        
        $post_links = $xpath->evaluate('//div[@class="post px-3 py-1 pl-md-0"]//p//a');

        $current_posts = Post::pluck('link')->toArray();
        $posts_array = [];
        $now = now()->toDateTimeString();

        foreach ($post_links as $link) {

            $post_url = $this->baseUrl.$link->getAttribute('href'); 

            if (!in_array($post_url, $current_posts) ) {
                $posts_array[] = $this->getPostItem($post_url, $now);
            }                            

        } 
        
        Post::insert($posts_array);

    }

    public function getPostItem($post_url, $now)
    {
        $response = Http::get($post_url);
        $htmlString = $response->body();        
        libxml_use_internal_errors(true);
        $doc = new \DOMDocument();
        $doc->loadHTML($htmlString);
        $xpath = new \DOMXPath($doc);

        $post_title = $xpath->evaluate('//h1')->item(0)->textContent;
        $post_content = $xpath->evaluate('//div[@class="job_description"]')->item(0);                        
        
        return [
            'title' => $post_title,
            'content' => $post_content,
            'link' => $post_url,
            'slug' => Str::slug($post_title. rand(1, 100)),
            'created_at' => $now,
            'updated_at' => $now,
        ];        

    }                    
}

Then I created test like this:

<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\Jobs\FetchPostsJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;


class FetchPostsJobTest extends TestCase
{

    use RefreshDatabase;    

    public function test_post_job_is_dispatched()
    {
        
        Bus::fake();        

        $this->artisan('schedule:work');
         
        Bus::assertDispatched(FetchPostsJob::class);        
        
    }   

    public function test_it_queues(){
        Queue::fake();

        $this->artisan('schedule:work');

        Queue::assertPushedOn('fetch-posts', FetchPostsJob::class);
      
    }    
   
    public function test_it_stores_data_in_database(){

      Bus::fake();

      $this->artisan('schedule:work');        

      $this->assertGreaterThan(Post::count(), 0);        
    
}            

}

app/Console/Kernel.php

<?php

namespace App\Console;

use App\Jobs\FetchPostsJob;
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 = [

    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {

      $schedule->job(new FetchPostsJob, 'fetch-posts')->everyThreeMinutes();        
        
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

But the tests runs forever, none of the tests works properly, what can I do? thank you.


Solution

  • Here's my suggestion on how to test this. If you have a different URL you can be testing on that would be even better but the basic use case would be something like:

    public function test_it_stores_data_in_database() {
          // Fake the HTTP helper
          Http::fake([ 
              '*' => Http::response(
                  'html><body><div><h1>Post title</h1><div class="job_description">Post content</div></div></body></html>', 
                  200)
          ]);
       
     
    
          Bus::fake();
          // Since getPostItem uses HTTP to get the pages you know that the response will be the above
          app()->make(FetchPostsJob::class)->getPostItem('example.com', Carbon::now());
    
          $this->assertGreaterThan(Post::count(), 0);    
    
          // You can also assert that the database contains a post with title "Post title" and content "Post content" since you control how the HTML for the test will look like    
        
    }