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()
{
if (Carbon::now()->isSameAs('09:00:00')) {
return 'Daily task executed at 9 AM.';
}
return 'No task scheduled at this time.';
}
}
Now, let's write the PHPUnit test case to test the executeDailyTask() method using Carbon::setTestNow().
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();
$this->assertEquals('Daily task executed at 9 AM.', $result);
Carbon::setTestNow();
}
public function testNoTaskScheduled()
{
Carbon::setTestNow('2023-07-26 12:00:00');
$scheduler = new TaskScheduler();
$result = $scheduler->executeDailyTask();
$this->assertEquals('No task scheduled at this time.', $result);
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:
use Carbon\Carbon;
class Subscription
{
private $startDate;
private $durationInMonths;
public function __construct($startDate, $durationInMonths)
{
$this->startDate = $startDate;
$this->durationInMonths = $durationInMonths;
}
public function hasExpired()
{
$endDate = $this->startDate->copy()->addMonths($this->durationInMonths);
return Carbon::now()->greaterThan($endDate);
}
}
Now, let's write the PHPUnit test case to test the hasExpired() method using Carbon::setTestNow().
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
class SubscriptionTest extends TestCase
{
public function testSubscriptionNotExpired()
{
$subscriptionStartDate = Carbon::create(2023, 07, 10);
$subscriptionDuration = 1;
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;
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:
class AuthController extends Controller {
public function login(Request $request)
{
$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)
{
$userLog = UserLog::where(
[
'activity_id' => $request->session()->get('activity_id')
]
)
$userLog->update(
[
'last_activity_at' => Carbon::now(),
]
)
}
}
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().
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!