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

trevorpan's avatar

The expected notification was not sent. [using cron scheduled command, phpunit]

Have checked out the below discussions and they have helped craft this test, but still get the error: https://laracasts.com/discuss/channels/testing/how-to-test-redis-queue-connection https://laracasts.com/discuss/channels/laravel/how-to-test-if-a-verification-email-was-queued

The expected [App\Notifications\SendJobBiddedWinningBidderNotified] notification was not sent. Failed asserting that false is true.

Basically, the bidded emails are scheduled by a cron job and when the conditions are right it dispatches the necessary emails. I've done some looking around, there does not appear to be a way to fake a cron job, though I did see Bus::fake(); in the docs I don't really understand what's happening there.

So I thought to try and make a private function in the test as shown below.

Any ideas on why the notification was not sent? Thank you ~

class PlaceBidEmailTest extends TestCase
{
    use RefreshDatabase;

    private function deadlineExpired()
    {
        $biddedJobs = SlugJob::where([
            ['deadline', '<=', Carbon::now('America/Los_Angeles')->toDateTimeString()],
            ['bidded', '=', 0]
        ])->get();

        foreach ($biddedJobs as $biddedJob):

            $bids = $biddedJob->bids;
            $winning = $bids->sortBy('bid')->first();
            $losing = $bids->sortBy('bid')->splice(1);

            foreach ($losing as $lost):
                $lost->user->notify(new SendJobBiddedLosingBiddersNotified($lost));
            endforeach;

            $winning->user->notify(new SendJobBiddedWinningBidderNotified($biddedJob));

            $biddedJob->user->notify(new SendJobBiddedBuyerNotified($biddedJob));

            $biddedJob->partialUpdateObjects(
                [
                    [
                        'bidded'  => 1,
                    ],
                ]
            );

        endforeach;

    }

/** @test */
    public function job_bidded_email_was_sent_to_winning_bidder_and_losing_bidders()
    {
//        $this->withoutExceptionHandling();

        $user = factory(User::class)->create([
            'email' => '[email protected]'
        ]);
        $user2 = factory(User::class)->create([
            'email' => '[email protected]'
        ]);
        $user3 = factory(User::class)->create([
            'email' => '[email protected]'
        ]);

        $this->actingAs($user);

        $job = factory(SlugJob::class)->create([
            'user_id' => $user
        ]);

        $winningBid = factory(Bid::class)->create([
            'user_id' => $user->id,
            'job_id' => $job->id,
            'bid' => 250000
        ]);

        $losingBid = factory(Bid::class)->create([
            'user_id' => $user2->id,
            'job_id' => $job->id,
            'bid' => 300000
        ]);

        $losingBid2 = factory(Bid::class)->create([
            'user_id' => $user3->id,
            'job_id' => $job->id,
            'bid' => 350000
        ]);

        $this->assertDatabaseHas('jobs', [
            'id' => $job->id
        ]);

        $this->assertDatabaseHas('users', [
             'email' => '[email protected]',
             'email' => '[email protected]',
             'email' => '[email protected]'
        ]);

        $biddedJob = $job; //I've reassigned this as the notification uses that variable name. Is this necessary?
//        dd($job);
//        dd($bid->user->firstname);

//        php artisan schedule email:deadlineHasPassed
//        dd($job->bids->first()->bid); , $winningBid, $losingBid, $losingBid2

        Notification::fake();
        Notification::assertNothingSent();

        $this->deadlineExpired();

//        dd($winningBid->user);

        Notification::assertSentTo(
            $winningBid->user,
            SendJobBiddedWinningBidderNotified::class,
                function ($notification) use ($biddedJob) {
                    return $notification->biddedJob->id === $biddedJob->id;
                }
        );
  }
// other tests
  
0 likes
9 replies
bobbybouwmann's avatar
Level 88

You cannot fake a cronjob, but you can run the scheduled job yourself like so

public function testMyCommand()
{
    $this->artisan('run:something')
        ->expectsOutput('Job completed')
        ->assertExitCode(0);
}

This way you can fake that it's triggered by the cronjob.

Bus::fake() can be used to catch all jobs that are being dispatched. They won't run the code, but you can check if they have been dispatched. You then need separate tests for each job.

1 like
trevorpan's avatar

Good morning @bobbybouwmann that's handy little tip!

// I changed the above to
    public function deadlineExpired()
    {
        $this->artisan('email:deadlineHasPassed')
            ->expectsOutput('Job completed')
            ->assertExitCode(0);
    }
...
//it does not seem to matter which order is placed.
        Notification::fake();
        Notification::assertNothingSent();

        $this->deadlineExpired();
//or
	$this->deadlineExpired();

        Notification::fake();
        Notification::assertNothingSent();

I've tried: (1) commenting out $this->withoutExceptionHandling(); (2) using it and (3) using$this->withExceptionHandling();. For some reason it never yields a more descriptive error than what is in the original post.

Then gave php artisan email:deadlineHasPassed a run on the command line (vs. a test) and find this line gives an: ErrorException : Trying to get property 'user' of non-object That really makes no sense.

//            dd($winning->user); // this pulls up the user object.
            $winning->user->notify(new SendJobBiddedWinningBidderNotified($winning));

Have you run into this before? What's really strange is the loop through the losing bidders (above this line) does not get the error and telescope does show a proper message was sent.

This is the output of $winning->user

App\User {#1207
  #fillable: array:6 [
    0 => "firstname"
    1 => "lastname"
    2 => "email"
    3 => "password"
    4 => "confirmation_number"
    5 => "invitation_code"
  ]
  #hidden: array:2 [
    0 => "password"
    1 => "remember_token"
  ]
  #connection: "mysql"
  #table: "users"
  #primaryKey: "id"
  #keyType: "int"
  +incrementing: true
  #with: []
  #withCount: []
  #perPage: 15
  +exists: true
  +wasRecentlyCreated: false
  #attributes: array:9 [
    "id" => 8
    "firstname" => "Dennis"
    "lastname" => "Smith"
    "email" => "[email protected]"
    "email_verified_at" => null
    "password" => "$argon2id$v=19$m=1024,t=2,p=2$UmJieUVhU0NhbTBHRTVtNg$UgDGRbCq+9cacMn3N31Nho8b/SauC3knOvvCPRL/leI"
    "remember_token" => null
    "created_at" => "2020-01-09 00:19:12"
    "updated_at" => "2020-01-09 00:19:12"
  ]
  #original: array:9 [
    "id" => 8
    "firstname" => "Dennis"
    "lastname" => "Smith"
    "email" => "[email protected]"
    "email_verified_at" => null
    "password" => "$argon2id$v=19$m=1024,t=2,p=2$UmJieUVhU0NhbTBHRTVtNg$UgDGRbCq+9cacMn3N31Nho8b/SauC3knOvvCPRL/leI"
    "remember_token" => null
    "created_at" => "2020-01-09 00:19:12"
    "updated_at" => "2020-01-09 00:19:12"
  ]
  #changes: []
  #casts: []
  #dates: []
  #dateFormat: null
  #appends: []
  #dispatchesEvents: []
  #observables: []
  #relations: []
  #touches: []
  +timestamps: true
  #visible: []
  #guarded: array:1 [
    0 => "*"
  ]
  #rememberTokenName: "remember_token"
}

bobbybouwmann's avatar

You need to put Notification::fake before you call the artisan command. It will only fake notifications before you perform any actions

public function testSomething()
{
    Notification::fake();

    // Perform actions call

    // Perform assurtions

    Notification::assertSent
}

Well you get the idea;)

trevorpan's avatar

Awesome, ok got the order proper now. @bobbybouwmann

Yea, it's really strange that the user object is being returned from dd($winning->user); but the errors spits out: ErrorException : Trying to get property 'user' of non-object

on this line

$winning->user->notify(new SendJobBiddedWinningBidderNotified($winning));

I thought possibly it's still a collection - perhaps of bids - but if I'm not mistaken ->first() only nabs one object - as the App\User details show above...

bobbybouwmann's avatar

Yeah, this looks correct to me. If you're $winning object always has a user, this shouldn't be a problem at all.

trevorpan's avatar

Hi @bobbybouwmann

There were definitely some issues with the expected model. After type hinting the Bid $winning in the constructor of Notification::class I realized it was expecting a Job not a Bid. So, got that straightened out.

Still not able to get the command to process properly. the logic $this->deadlineExpired(); never processes anything.

I found this post: https://github.com/laravel/framework/issues/17778 towards the bottom it shows pulling the latest user - I think I was grabbing the wrong user.

If the notifications are sent manually the test passes...

    /** @test */
    public function job_bidded_email_was_sent_to_winning_bidder_and_losing_bidders()
    {
        $this->withoutExceptionHandling();
        Notification::fake();
        Notification::assertNothingSent();
//        Queue::fake();
//        Queue::assertNothingPushed();

        $user = factory(User::class)->create([
            'email' => '[email protected]'
        ]);
        $user2 = factory(User::class)->create([
            'email' => '[email protected]'
        ]);
        $user3 = factory(User::class)->create([
            'email' => '[email protected]'
        ]);
        $user4 = factory(User::class)->create([
            'email' => '[email protected]'
        ]);

        $this->actingAs($user);

        $job = factory(SlugJob::class)->create([
            'user_id' => $user->id
        ]);

//        dd($user2->id);

        $winningBid = factory(Bid::class)->create([
            'user_id' => $user2->id,
            'job_id' => $job->id,
            'bid' => 250000
        ]);

        $losingBid = factory(Bid::class)->create([
            'user_id' => $user3->id,
            'job_id' => $job->id,
            'bid' => 300000
        ]);

        $losingBid2 = factory(Bid::class)->create([
            'user_id' => $user4->id,
            'job_id' => $job->id,
            'bid' => 350000
        ]);

//        dd($losingBid2->job->id);

        $this->assertDatabaseHas('jobs', [
            'id' => $job->id,
            'bidded' => 0
        ]);

        $this->assertDatabaseHas('bids', [
            'id' => $winningBid->id,
            'bid' => 250000,
            'id' => $losingBid->id,
            'bid' => 300000,
            'id' => $losingBid2->id,
            'bid' => 350000,
        ]);

        $this->assertDatabaseHas('users', [
            'email' => '[email protected]',
            'email' => '[email protected]',
            'email' => '[email protected]',
            'email' => '[email protected]'
        ]);

//        $this->deadlineExpired();

//        dd($losingBid2->user);

        $winningBid->user->notify(new SendJobBiddedWinningBidderNotified($winningBid));
        $job->user->notify(new SendJobBiddedBuyerNotified($job));
        $losingBid->user->notify(new SendJobBiddedLosingBiddersNotified($losingBid));
        $losingBid2->user->notify(new SendJobBiddedLosingBiddersNotified($losingBid2));

        Notification::assertSentTo(User::latest()->first(), SendJobBiddedBuyerNotified::class);
	// also works Notification::assertSentTo(User::find(1), SendJobBiddedBuyerNotified::class);
        Notification::assertSentTo(User::find(2), SendJobBiddedWinningBidderNotified::class);
        Notification::assertSentTo(User::find(3), SendJobBiddedLosingBiddersNotified::class);
        Notification::assertSentTo(User::find(4), SendJobBiddedLosingBiddersNotified::class);
// so the above does yield a passing test
Time: 484 ms, Memory: 32.00 MB

OK (1 test, 8 assertions)

Would you worry about the console command not working in the context of a test?

trevorpan's avatar

gahhhh @bobbybouwmann

Man, you really help me focus and think better.

Here's the culprit:

        $job = factory(SlugJob::class)->create([
            'user_id' => $user->id,
            'deadline' => Carbon::now('America/Los_Angeles')->toDateTimeString()
        ]);

Previously, in the factory, the deadline was based on a random number of weeks being added to the timeline - so bidders have time to place a bid. So, in this particular test, I overrode the deadline - pretty simple actually but totally a gotcha.... now the console command is called properly - as you mentioned - and works perfect.

I noticed this when temporarily importing the database condition where deadline was <= to NOW().

Thank you `

BenjaminS's avatar

Maybe unnecessary, but when testing commands using $this->artisan() be sure execute the actual command before asserting the notifications;

        // Arrange
        Notification::fake();

        // Act
        $command = $this->artisan('app:my-special-command');

        // Assert
        $command->assertExitCode(0);

        // Execute to validate notifications
        $command->execute();
        Notification::assertSent(MyNotificationClass::class);

This fixed my simple tests for me!

Please or to participate in this conversation.