Back to Course |
Laravel User Timezones Project: Convert, Display, Send Notifications

Automated Tests for Notification System

To ensure that our Notifications are sent when they are supposed to - we have to write automated tests. While this part is optional, it is highly recommended to do so to ensure that the system is working as expected. Manually testing the following cases could be hard and time-consuming:

  • Creating a booking creates the scheduled Notifications
  • Creating a booking that is sooner than some Notifications don't schedule them
  • Scheduler sends the Notifications out
  • Notifications are sent out even if time passed (e.g. something went wrong with the Scheduler, and it didn't run for a few hours) This might not be good for your case, adjust as needed!
  • Making sure that Notifications that fail are retried, and we don't try more than 5 times. Our Queue worker might get overloaded otherwise
  • Updating a booking updates the scheduled Notifications
  • Deleting a booking deletes the scheduled Notifications

Testing the Scheduled Notifications

To test this, we will create a new file:

tests/Feature/BookingNotificationsTest.php

namespace Tests\Feature;
 
use App\Models\Booking;
use App\Models\User;
use App\Notifications\BookingReminder1H;
use App\Notifications\BookingReminder2H;
use App\Notifications\BookingReminder5MIN;
use App\Notifications\BookingStartedNotification;
use Artisan;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
 
class BookingNotificationsTest extends TestCase
{
use RefreshDatabase;
 
public function test_creating_a_booking_schedules_notifications(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$this->assertEquals(4, $user->scheduledNotifications()->count());
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($startTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
$oneHourTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHour()), 'UTC')->setSeconds(0);
$twoHoursTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHours(2)), 'UTC')->setSeconds(0);
 
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first());
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first());
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first());
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first());
}
 
public function test_creating_booking_that_is_sooner_than_some_notifications_dont_schedule_them(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addMinutes(30);
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$this->assertEquals(2, $user->scheduledNotifications()->count());
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($startTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
 
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first());
$this->assertNotNull($user->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first());
}
 
public function test_scheduler_sends_the_notifications_out(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$booking = Booking::where('user_id', $user->id)->first();
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($startTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
$oneHourTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHour()), 'UTC')->setSeconds(0);
$twoHoursTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHours(2)), 'UTC')->setSeconds(0);
 
date_default_timezone_set('UTC');
 
Notification::fake();
Carbon::setTestNow(now()->addHours(6));
Artisan::call('send:scheduled-notifications');
Notification::assertNothingSent();
 
Notification::fake();
Carbon::setTestNow($twoHoursTimeToCheck);
Artisan::call('send:scheduled-notifications');
Notification::assertSentTo([$user], BookingReminder2H::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first()->sent);
 
Notification::fake();
Artisan::call('send:scheduled-notifications');
Notification::assertNothingSentTo($user);
 
Notification::fake();
Carbon::setTestNow($oneHourTimeToCheck);
Artisan::call('send:scheduled-notifications');
Notification::assertSentTo([$user], BookingReminder1H::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first()->sent);
 
Notification::fake();
Carbon::setTestNow($fiveMinTimeToCheck);
Artisan::call('send:scheduled-notifications');
Notification::assertSentTo([$user], BookingReminder5MIN::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first()->sent);
 
Notification::fake();
Carbon::setTestNow($startTimeToCheck);
Artisan::call('send:scheduled-notifications');
Notification::assertSentTo([$user], BookingStartedNotification::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first()->sent);
}
 
public function test_sends_out_all_notifications_if_something_went_wrong(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$booking = Booking::where('user_id', $user->id)->first();
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($startTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
$oneHourTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHour()), 'UTC')->setSeconds(0);
$twoHoursTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHours(2)), 'UTC')->setSeconds(0);
 
date_default_timezone_set('UTC');
 
Notification::fake();
Carbon::setTestNow($startTimeToCheck);
Artisan::call('send:scheduled-notifications');
Notification::assertSentTo([$user], BookingReminder2H::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first()->sent);
 
Notification::assertSentTo([$user], BookingReminder1H::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first()->sent);
 
Notification::assertSentTo([$user], BookingReminder5MIN::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first()->sent);
 
Notification::assertSentTo([$user], BookingStartedNotification::class);
$this->assertTrue($booking->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first()->sent);
}
 
public function test_tries_are_increasing_when_failed(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$booking = Booking::where('user_id', $user->id)->first();
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($startTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
$oneHourTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHour()), 'UTC')->setSeconds(0);
$twoHoursTimeToCheck = Carbon::parse(fromUserDateTime($startTime->copy()->subHours(2)), 'UTC')->setSeconds(0);
 
date_default_timezone_set('UTC');
 
$booking->delete();
 
Notification::fake();
Carbon::setTestNow($startTimeToCheck);
 
for ($i = 1; $i <= 2; $i++) {
// Each time we run the command, the tries will be increased by 2
// as it will attempt to send the notification 2 times
Artisan::call('send:scheduled-notifications');
Notification::assertNothingSent();
$this->assertEquals($i * 2, $booking->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first()->tries);
$this->assertEquals($i * 2, $booking->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first()->tries);
$this->assertEquals($i * 2, $booking->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first()->tries);
$this->assertEquals($i * 2, $booking->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first()->tries);
}
 
// After 3 tries, we make sure that we didn't exceed our limit of attempts:
Artisan::call('send:scheduled-notifications');
Notification::assertNothingSent();
$this->assertEquals(config('app.notificationAttemptAmount'), $booking->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first()->tries);
$this->assertEquals(config('app.notificationAttemptAmount'), $booking->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first()->tries);
$this->assertEquals(config('app.notificationAttemptAmount'), $booking->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first()->tries);
$this->assertEquals(config('app.notificationAttemptAmount'), $booking->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first()->tries);
}
 
public function test_editing_booking_start_date_changes_scheduled_notifications(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$this->assertEquals(4, $user->scheduledNotifications()->count());
 
$booking = Booking::where('user_id', $user->id)->first();
 
$newStartTime = now()->addDays(2);
$this->actingAs($user)
->put(route('booking.update', $booking->id), [
'start' => $newStartTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$startTimeToCheck = Carbon::parse(fromUserDateTime($newStartTime, $user), 'UTC')->setSeconds(0);
$fiveMinTimeToCheck = Carbon::parse(fromUserDateTime($newStartTime->copy()->subMinutes(5)), 'UTC')->setSeconds(0);
$oneHourTimeToCheck = Carbon::parse(fromUserDateTime($newStartTime->copy()->subHour()), 'UTC')->setSeconds(0);
$twoHoursTimeToCheck = Carbon::parse(fromUserDateTime($newStartTime->copy()->subHours(2)), 'UTC')->setSeconds(0);
 
$this->assertNotNull($booking->scheduledNotifications()->where('scheduled_at', $startTimeToCheck)->first());
$this->assertNotNull($booking->scheduledNotifications()->where('scheduled_at', $fiveMinTimeToCheck)->first());
$this->assertNotNull($booking->scheduledNotifications()->where('scheduled_at', $oneHourTimeToCheck)->first());
$this->assertNotNull($booking->scheduledNotifications()->where('scheduled_at', $twoHoursTimeToCheck)->first());
}
 
public function test_deleting_a_booking_deletes_scheduled_events(): void
{
$user = User::factory()->create([
'timezone' => 'America/New_York'
]);
$startTime = now()->addDay();
$this->actingAs($user)
->post(route('booking.store'), [
'start' => $startTime,
'end' => now()->addDay()->addHours(2)
])
->assertRedirect(route('booking.index'));
 
$booking = Booking::where('user_id', $user->id)->first();
 
$this->actingAs($user)
->delete(route('booking.destroy', $booking->id));
 
$this->assertEquals(0, $booking->scheduledNotifications()->where('notifiable_id', $booking->id)->count());
}
}

This automated test covers the previously mentioned cases and comes in handy if we want to test the functionality of the command in the future. For example, you might want to extend this to include a variety of different time zones and see if the Notifications are sent at the correct time. To run them, you can use the following command:

php artisan test

Once it runs - you should see that all tests are passing!

That's it! The system is fully complete and ready to be used. You can find the full source code for this project on GitHub