carbonite icon indicating copy to clipboard operation
carbonite copied to clipboard

Freeze, accelerate, slow down and much more with Carbon

Carbonite

Freeze, accelerate, slow down time and many more with Carbon.

Latest Stable Version GitHub Actions Code Climate Test Coverage Issue Count StyleCI

Carbonite allows you to write unit tests as you would tell a story for times concerns.

Professionally supported nesbot/carbon is now available

Install

composer require --dev kylekatarnls/carbonite

We install Carbonite with --dev because it's designed for tests. You watched enough Sci-Fi movies to know time travel paradoxes are too dangerous for production.

Usage

<?php

use Carbon\Carbon;
use Carbon\Carbonite;

function scanEpoch() {
    switch (Carbon::now()->year) {
        case 1944:
            return 'WW2';
        case 1946:
            return 'War is over';
        case 2255:
            return 'The Discovery is flying!';
    }
}

Carbonite::freeze('1944-05-05');

echo scanEpoch(); // output: WW2

Carbonite::elapse('2 years');

echo scanEpoch(); // output: War is over

Carbonite::jumpTo('2255-01-01 00:00:00');

echo scanEpoch(); // output: The Discovery is flying!

Carbonite::speed(3); // Times passes now thrice as fast
sleep(15); // If 15 seconds passes in the real time

// Then 45 seconds passed in our fake timeline:
echo Carbon::now(); // output: 2255-01-01 00:00:45

You can also use CarbonImmutable, both will be synchronized.

And as Carbonite directly handle any date created with Carbon, it will work just fine for properties like created_at, updated_at or any custom date field in your Laravel models or any framework using Carbon.

Example of Laravel unit test

Example of Laravel feature test

Example of raw PHPUnit test

Available methods

freeze

Carbonite::freeze($toMoment = 'now', float $speed = 0.0): void

Freeze the time to a given moment (now by default).

// Assuming now is 2005-04-01 15:56:23

Carbonite::freeze(); // Freeze our fake timeline on current moment 2005-04-01 15:56:23

// You run a long function, so now is 2005-04-01 15:56:24
echo Carbon::now(); // output: 2005-04-01 15:56:23
// Time is frozen for any relative time in Carbon

Carbonite::freeze('2010-05-04');

echo Carbon::now()->isoFormat('MMMM [the] Do'); // output: May the 4th

This is particularly useful to avoid the small microseconds/seconds gaps that appear randomly in unit tests when you do date-time comparison.

For example:

$now = Carbon::now()->addSecond();

echo (int) Carbon::now()->diffInSeconds($now); // output: 0

// because first Carbon::now() is a few microseconds before the second one,
// so the final diff is a bit less than 1 second

Carbonite::freeze();

$now = Carbon::now()->addSecond();

echo (int) Carbon::now()->diffInSeconds($now); // output: 1

// Time is frozen so the whole thing behaves as if it was instantaneous

The first argument can be a string, a DateTime/DateTimeImmutable, Carbon/CarbonImmutable instance. But it can also be a DateInterval/CarbonInterval (to add to now) or a DatePeriod/CarbonPeriod to jump to the start of this period.

As a second optional parameter you can choose the new time speed after the freeze (0 by default).

Carbonite::freeze('2010-05-04', 2); // Go to 2010-05-04 then make the time pass twice as fast.

See speed() method.

speed

Carbonite::speed(float $speed = null): float

Called without arguments Carbonite::speed() gives you the current speed of the fake timeline (0 if frozen).

With an argument, it will set the speed to the given value:

// Assuming now is 19 October 1977 8pm
Carbonite::freeze();
// Sit in the movie theater
Carbonite::speed(1 / 60); // Now every minute flies away like it's just a second
// 121 minutes later, now is 19 October 1977 10:01pm
// But it's like it's just 8:02:01pm in your timeline
echo Carbon::now()->isoFormat('h:mm:ssa'); // output: 8:02:01pm

// Now it's 19 October 1977 11:00:00pm
Carbonite::jumpTo('19 October 1977 11:00:00pm');
Carbonite::speed(3600); // and it's like every second was an hour
// 4 seconds later, now it's 19 October 1977 11:00:04pm
// it's like it's already 3am the next day
echo Carbon::now()->isoFormat('YYYY-MM-DD h:mm:ssa'); // output: 1977-10-20 3:00:00am

fake

Carbonite::fake(CarbonInterface $realNow): CarbonInterface

Get fake now instance from real now instance.

// Assuming now is 2020-03-14 12:00

Carbonite::freeze('2019-12-23'); // Set fake timeline to last December 23th (midnight)
Carbonite::speed(1); // speed = 1 means each second elapsed in real file, elpase 1 second in the fake timeline

// Then we can see what date and time it would be in the fake time line
// if we were let's say March the 16th in real life:

echo Carbonite::fake(Carbon::parse('2020-03-16 14:00')); // output: 2019-12-25 02:00:00
// Cool it would be Christmas (2am) in our fake timeline

accelerate

accelerate(float $factor): float

Speeds up the time in the fake timeline by the given factor; and returns the new speed. accelerate(float $factor): float

Carbonite::speed(2);

echo Carbonite::accelerate(3); // output: 6

decelerate

decelerate(float $factor): float

Slows down the time in the fake timeline by the given factor; and returns the new speed. decelerate(float $factor): float

Carbonite::speed(5);

echo Carbonite::decelerate(2); // output: 2.5

unfreeze

unfreeze(): void

Unfreeze the fake timeline.

// Now it's 8:00am
Carbonite::freeze();
echo Carbonite::speed(); // output: 0

// Now it's 8:02am
// but time is frozen
echo Carbon::now()->format('g:i'); // output: 8:00

Carbonite::unfreeze();
echo Carbonite::speed(); // output: 1

// Our timeline restart where it was paused
// so now it's 8:03am
echo Carbon::now()->format('g:i'); // output: 8:01

jumpTo

jumpTo($moment, float $speed = null): void

Jump to a given moment in the fake timeline keeping the current speed.

Carbonite::freeze('2000-06-30');
Carbonite::jumpTo('2000-09-01');
echo Carbon::now()->format('Y-m-d'); // output: 2000-09-01
Carbonite::jumpTo('1999-12-20');
echo Carbon::now()->format('Y-m-d'); // output: 1999-12-20

A second parameter can be passed to change the speed after the jump. By default, speed is not changed.

elapse

elapse($duration, float $speed = null): void

Add the given duration to the fake timeline keeping the current speed.

Carbonite::freeze('2000-01-01');
Carbonite::elapse('1 month');
echo Carbon::now()->format('Y-m-d'); // output: 2000-02-01
Carbonite::elapse(CarbonInterval::year());
echo Carbon::now()->format('Y-m-d'); // output: 2001-02-01
Carbonite::elapse(new DateInterval('P1M3D'));
echo Carbon::now()->format('Y-m-d'); // output: 2001-03-04

A second parameter can be passed to change the speed after the jump. By default, speed is not changed.

rewind

rewind($duration, float $speed = null): void

Subtract the given duration to the fake timeline keeping the current speed.

Carbonite::freeze('2000-01-01');
Carbonite::rewind('1 month');
echo Carbon::now()->format('Y-m-d'); // output: 1999-12-01
Carbonite::rewind(CarbonInterval::year());
echo Carbon::now()->format('Y-m-d'); // output: 1998-12-01
Carbonite::rewind(new DateInterval('P1M3D'));
echo Carbon::now()->format('Y-m-d'); // output: 1998-10-29

A second parameter can be passed to change the speed after the jump. By default, speed is not changed.

do

do($moment, callable $action)

Trigger a given $action in a frozen instant $testNow. And restore previous moment and speed once it's done, rather it succeeded or threw an error or an exception.

Returns the value returned by the given $action.

Carbonite::freeze('2000-01-01', 1.5);
Carbonite::do('2020-12-23', static function () {
    echo Carbon::now()->format('Y-m-d H:i:s.u'); // output: 2020-12-23 00:00:00.000000
    usleep(200);
    // Still the same output as time is frozen inside the callback
    echo Carbon::now()->format('Y-m-d H:i:s.u'); // output: 2020-12-23 00:00:00.000000
    echo Carbonite::speed(); // output: 0
});
// Now the speed is 1.5 on 2000-01-01 again
echo Carbon::now()->format('Y-m-d'); // output: 2000-01-01
echo Carbonite::speed(); // output: 1.5

Carbonite::do() is a good way to isolate a test and use a particular date as "now" then be sure to restore the previous state. If there is no previous Carbonite state (if you didn't do any freeze, jump, speed, etc.) then Carbon::now() will just no longer be mocked at all.

doNow

doNow(callable $action)

Trigger a given $action in the frozen current instant. And restore previous speed once it's done, rather it succeeded or threw an error or an exception.

Returns the value returned by the given $action.

// Assuming now is 17 September 2020 8pm
Carbonite::doNow(static function () {
    echo Carbon::now()->format('Y-m-d H:i:s.u'); // output: 2020-09-17 20:00:00.000000
    usleep(200);
    // Still the same output as time is frozen inside the callback
    echo Carbon::now()->format('Y-m-d H:i:s.u'); // output: 2020-09-17 20:00:00.000000
    echo Carbonite::speed(); // output: 0
});
// Now the speed is 1 again
echo Carbonite::speed(); // output: 1

It's actually a shortcut for Carbonite::do('now', callable $action).

Carbonite::doNow() is a good way to isolate a test, stop the time for this test then be sure to restore the previous state. If there is no previous Carbonite state (if you didn't do any freeze, jump, speed, etc.) then Carbon::now() will just no longer be mocked at all.

release

release(): void

Go back to the present and normal speed.

// Assuming now is 2019-05-24
Carbonite::freeze('2000-01-01');
echo Carbon::now()->format('Y-m-d'); // output: 2000-01-01
echo Carbonite::speed(); // output: 0

Carbonite::release();
echo Carbon::now()->format('Y-m-d'); // output: 2019-05-24
echo Carbonite::speed(); // output: 1

mock

mock($testNow): void

Set the "real" now moment, it's a mock inception. It means that when you call release() You will no longer go back to present but you will fallback to the mocked now. And the mocked now will also determine the base speed to consider. If this mocked instance is static, then "real" time will be frozen and so the fake timeline too no matter the speed you chose.

This is a very low-level feature used for the internal unit tests of Carbonite and you probably won't need this methods in your own code and tests, you more likely need the freeze() or jumpTo() method.

PHPUnit example

use Carbon\Carbonite;
use Carbon\CarbonPeriod;
use PHPUnit\Framework\TestCase;

class MyProjectTest extends TestCase
{
    protected function setUp(): void
    {
        // Working with frozen time in unit tests is highly recommended.
        Carbonite::freeze();
    }

    protected function tearDown(): void
    {
        // Release after each test to isolate the timeline of each one.
        Carbonite::release();
    }

    public function testHolidays()
    {
        $holidays = CarbonPeriod::create('2019-12-23', '2020-01-06', CarbonPeriod::EXCLUDE_END_DATE);
        Carbonite::jumpTo('2019-12-22');

        $this->assertFalse($holidays->isStarted());

        Carbonite::elapse('1 day');

        $this->assertTrue($holidays->isInProgress());

        Carbonite::jumpTo('2020-01-05 22:00');

        $this->assertFalse($holidays->isEnded());

        Carbonite::elapse('2 hours');

        $this->assertTrue($holidays->isEnded());

        Carbonite::rewind('1 microsecond');

        $this->assertFalse($holidays->isEnded());
    }
}

PHP 8 attributes (or PHPDoc annotations for PHP 7) can also be used for convenience. Enable it using Bespin::up() on a given test suite:

PHP 8

use Carbon\Bespin;
use Carbon\Carbon;
use Carbon\Carbonite;
use Carbon\Carbonite\Attribute\Freeze;
use Carbon\Carbonite\Attribute\JumpTo;
use Carbon\Carbonite\Attribute\Speed;
use PHPUnit\Framework\TestCase;

class PHP8Test extends TestCase
{
    protected function setUp(): void
    {
        // Will handle attributes on each method before running it
        Bespin::up($this);
    }

    protected function tearDown(): void
    {
        // Release the time after each test
        Bespin::down();
    }

    #[Freeze("2019-12-25")]
    public function testChristmas()
    {
        // Here we are the 2019-12-25, time is frozen.
        self::assertSame('12-25', Carbon::now()->format('m-d'));
        self::assertSame(0.0, Carbonite::speed());
    }

    #[JumpTo("2021-01-01")]
    public function testJanuaryFirst()
    {
        // Here we are the 2021-01-01, but time is NOT frozen.
        self::assertSame('01-01', Carbon::now()->format('m-d'));
        self::assertSame(1.0, Carbonite::speed());
    }

    #[Speed(10)]
    public function testSpeed()
    {
        // Here we start from the real date-time, but during
        // the test, time elapse 10 times faster.
        self::assertSame(10.0, Carbonite::speed());
    }

    #[Release()]
    public function testRelease()
    {
        // If no attributes have been used, Bespin::up() will use:
        // Carbonite::freeze('now')
        // But you can still use @Release() to get a test with
        // real time
    }
}

PHP 7

use Carbon\Bespin;
use Carbon\Carbon;
use Carbon\Carbonite;
use Carbon\Carbonite\Attribute\Freeze;
use Carbon\Carbonite\Attribute\JumpTo;
use Carbon\Carbonite\Attribute\Speed;
use PHPUnit\Framework\TestCase;

class PHP7Test extends TestCase
{
    protected function setUp(): void
    {
        // Will handle annotations on each method before running it
        Bespin::up($this);
    }

    protected function tearDown(): void
    {
        // Release the time after each test
        Bespin::down();
    }

    /** @Freeze("2019-12-25") */
    public function testChristmas()
    {
        // Here we are the 2019-12-25, time is frozen.
        self::assertSame('12-25', Carbon::now()->format('m-d'));
        self::assertSame(0.0, Carbonite::speed());
    }

    /** @JumpTo("2021-01-01") */
    public function testJanuaryFirst()
    {
        // Here we are the 2021-01-01, but time is NOT frozen.
        self::assertSame('01-01', Carbon::now()->format('m-d'));
        self::assertSame(1.0, Carbonite::speed());
    }

    /** @Speed(10) */
    public function testSpeed()
    {
        // Here we start from the real date-time, but during
        // the test, time elapse 10 times faster.
        self::assertSame(10.0, Carbonite::speed());
    }

    /** @Release() */
    public function testRelease()
    {
        // If no annotations have been used, Bespin::up() will use:
        // Carbonite::freeze('now')
        // But you can still use @Release() to get a test with
        // real time
    }
}

Annotations also work with multiline blocks, so you (with both PHPDoc or attributes) have in the same test a @group, @dataProvider or whatever.

fakeAsync() for PHP

If you're familiar with fakeAsync() and tick() of Angular testing tools, then you can get the same syntax in your PHP tests using:

use Carbon\Carbonite;

function fakeAsync(callable $fn): void {
    Carbonite::freeze();
    $fn();
    Carbonite::release();
}

function tick(int $milliseconds): void {
    Carbonite::elapse("$milliseconds milliseconds");
}

And use it as below:

use Carbon\Carbon;

fakeAsync(function () {
    $now = Carbon::now();
    tick(2000);

    echo $now->diffForHumans(); // output: 2 seconds ago
});