Testing Time Logic with Carbon's setTestNow()

Testing Time Logic with Carbon's setTestNow()

Time-related logic is a fundamental aspect of many applications. From handling different behavior on weekends to scheduling tasks at specific times or simply logging activities, the current time often plays a crucial role.

However, it can become a hurdle during testing when we need to simulate different time scenarios. In Laravel, Carbon has a hidden function called Carbon::setTestNow() which allows us to manipulate time effortlessly for testing purposes.

Understanding Carbon::setTestNow()

Carbon::setTestNow() is a method that allows you to override the current time with a custom time for testing purposes. This means you can trick your application into thinking it's any time you desire, without having to wait for the actual time to match your testing scenario.

Syntax: The syntax for Carbon::setTestNow() is straightforward. It takes a parameter of type Carbon or a string representing the desired time and sets it as the current time for your application.

Scenario 1 ~ Task Scheduling

Let's implement and test a scenario where we have a task scheduler that executes a daily task at 9 AM. We'll use Carbon::setTestNow() to test the scheduled task without waiting in real-time.

First, let's create a simple task scheduler class that contains a method for executing the daily task:

use Carbon\Carbon;

class TaskScheduler
{
    public function executeDailyTask()
    {
        // Check if the current time is 9 AM
        if (Carbon::now()->isSameAs('09:00:00')) {
            // Execute the daily task
            return 'Daily task executed at 9 AM.';
        }

        // No task to execute at this time
        return 'No task scheduled at this time.';
    }
}

Now, let's write the PHPUnit test case to test the executeDailyTask() method using Carbon::setTestNow().

// TaskSchedulerTest.php
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class TaskSchedulerTest extends TestCase
{
    public function testExecuteDailyTaskAt9AM()
    {
        Carbon::setTestNow('2023-07-26 09:00:00');
        $scheduler = new TaskScheduler();

        $result = $scheduler->executeDailyTask();

        // Assert that the daily task was executed
        $this->assertEquals('Daily task executed at 9 AM.', $result);

        // Reset to real-time
        Carbon::setTestNow();
    }

    public function testNoTaskScheduled()
    {
        Carbon::setTestNow('2023-07-26 12:00:00');
        $scheduler = new TaskScheduler();

        $result = $scheduler->executeDailyTask();

        // Assert that no task is scheduled at this time
        $this->assertEquals('No task scheduled at this time.', $result);

        // Reset to real-time
        Carbon::setTestNow();
    }
}

Scenario 2 ~ Subscription Expiry

Let's implement and test the scenario where we have a subscription-based application, and we want to test if user subscriptions expire correctly after a specific duration. We'll use Carbon::setTestNow() to simulate different subscription periods without waiting for the actual time to pass.

First, let's create a Subscription class that represents a user's subscription and contains a method to check if the subscription has expired:

// Subscription.php
use Carbon\Carbon;

class Subscription
{
    private $startDate;
    private $durationInMonths;

    public function __construct($startDate, $durationInMonths)
    {
        $this->startDate = $startDate;
        $this->durationInMonths = $durationInMonths;
    }

    public function hasExpired()
    {
        // Calculate the end date of the subscription
        $endDate = $this->startDate->copy()->addMonths($this->durationInMonths);

        // Check if the current time has passed the end date
        return Carbon::now()->greaterThan($endDate);
    }
}

Now, let's write the PHPUnit test case to test the hasExpired() method using Carbon::setTestNow().

// SubscriptionTest.php
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class SubscriptionTest extends TestCase
{
    public function testSubscriptionNotExpired()
    {
        $subscriptionStartDate = Carbon::create(2023, 07, 10);
        $subscriptionDuration = 1; // 1 month

        Carbon::setTestNow($subscriptionStartDate->copy()->addDays(10));
        $subscription = new Subscription($subscriptionStartDate, $subscriptionDuration);

        $isExpired = $subscription->hasExpired();

        $this->assertFalse($isExpired);
    }

    public function testSubscriptionExpired()
    {
        $subscriptionStartDate = Carbon::create(2023, 06, 01);
        $subscriptionDuration = 3; // 3 months

        Carbon::setTestNow($subscriptionStartDate->copy()->addMonths(4));

        $subscription = new Subscription($subscriptionStartDate, $subscriptionDuration);
        $isExpired = $subscription->hasExpired();

        $this->assertTrue($isExpired);
    }
}

Scenario 3 ~ User Login/Logout Logs

Let's implement and test the scenario where we have an application, and we want to track when the user logs in and logs out of the system. We'll use Carbon::setTestNow() to simulate different times for user login and log out.

First, let's create a AuthController class for user login and logout:

// AuthController
class AuthController extends Controller {

    public function login(Request $request) 
    {
        /** code to login user **/ 
        $activityId = Str::random();
        UserLog::create(
            [
                'activity_id' => $activityId,
                'email' => $request->get('email'),
                'login_at' => Carbon::now(),
                'last_activity_at' => Carbon::now(),
            ]
        )

        $request->session()->put('activity_id', $activityId);
    }

    public function logout(Request $request) 
    {
        /** code to logout user **/
        $userLog = UserLog::where(
            [
                'activity_id' => $request->session()->get('activity_id')
            ]
        )

        $userLog->update(
            [
                'last_activity_at' => Carbon::now(),
            ]
        )
    }
}

//routes.php 
Route::post('login', [AuthController::class, 'login']);
Route::get('logout', [AuthController::class, 'logout']);

Now, let's write the PHPUnit test case to test the last_activity_at field gets updated properly using Carbon::setTestNow().

// UserLogTest.php
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;

class UserLogTest extends TestCase
{
    public function testUserLogoutUpdatesUserLog()
    {
        $startTime = now();
        $endTime   = $startTime->addMinutes(10);
        $user      = UserFactory::create();

        Carbon::setTestNow($startTime);
        $this->post('login', [
            'email'    => $user->email,
            'password' => 'password',
        ]);

        Carbon::setTestNow($endTime);
        $this->get('logout');

        $this->assertDatabaseHas('user_logs', [
            'email'            => $user->email,
            'last_activity_at' => $endTime->toDateTimeString(),
        ]);
    }
}

Conclusion

So, the next time you encounter time-sensitive logic in your application, remember to make use of Carbon::setTestNow() and unlock a world of possibilities for seamless testing.

Happy coding!