21
ClockMock: a library to mock date and time in PHP
If you write tests for your code (I hope you do), at some point you likely needed something to execute them with the system clock “frozen” to a specific date and time. Whoever had this need at least once knows that the matter is anything but trivial.
With this article, I want to tell the story of how we dealt with this problem at Slope. For doing this, I will use the following “codebase” (i.e. a pompous way to describe 2 classes — an entity and a service).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Entity; | |
use DateTimeImmutable; | |
class Entity | |
{ | |
private DateTimeImmutable $creationDate; | |
private ?DateTimeImmutable $processingDate = null; | |
public function __construct() | |
{ | |
$this->creationDate = new DateTimeImmutable(); | |
} | |
public function getCreationDate(): DateTimeImmutable | |
{ | |
return $this->creationDate; | |
} | |
public function getProcessingDate(): ?DateTimeImmutable | |
{ | |
return $this->processingDate; | |
} | |
public function scheduleNextProcessing(DateTimeImmutable $processingDate): void | |
{ | |
$this->processingDate = $processingDate; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Service; | |
use DateTimeImmutable; | |
use Exception; | |
use App\Entity\Entity; | |
class Service | |
{ | |
public function doSomething(): void | |
{ | |
$now = new DateTimeImmutable(); | |
if ((int) $now->format('d') > 28) { | |
throw new Exception('This thing cannot be done after the 28th of every month.'); | |
} | |
// ... Something is done here | |
} | |
public function scheduleProcessing(Entity $entity): void | |
{ | |
$entity->scheduleNextProcessing(new DateTimeImmutable('+5 minutes')); | |
} | |
} |
I will describe 3 testing scenarios (a, b. and c.) that pretty much cover 100% of our cases/needs. Your mileage may vary, but the same concepts should apply.
Our journey starts from a situation in which we simply accepted to test the following scenarios in a sub-optimal way — or to not test them at all.
Our “basic” go-to solution here was reflection, used to artificially modify the date (stored in a private instance property) right after calling the entity constructor. Example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use ReflectionClass; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
class SomeIntegrationTest extends TestCase | |
{ | |
public function test_something_related_to_entity_creation_date() | |
{ | |
$entity = new Entity(); | |
$reflection = new ReflectionClass($entity); | |
$property = $reflection->getProperty('creationDate'); | |
$property->setAccessible(true); | |
$property->setValue($entity, new DateTimeImmutable('1 year ago'); | |
// Now $entity->getCreationDate() returns '1 year ago'. Your fixture is ready! | |
} | |
} |
In such situations, we usually avoided to test these services altogether. In some other cases, we modified code to allow passing the current date as a parameter. We did not like making this kind of changes, because at that point you don’t know anymore whether the current date is effectively a parameter even in “production” usage, or it was made that way only for testing.
I’m not embedding any code for this scenario, because as the beginning it was simply untestable.
In the past, we worked around this by testing that the date set by the service was “close enough” to the current date (e.g. within 500 milliseconds), with the assumption that the assertion was made shortly after SUT code execution.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use ReflectionClass; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
class SomeIntegrationTest extends TestCase | |
{ | |
public function test_something_related_to_entity_creation_date() | |
{ | |
$entity = new Entity(); | |
$reflection = new ReflectionClass($entity); | |
$property = $reflection->getProperty('creationDate'); | |
$property->setAccessible(true); | |
$property->setValue($entity, new DateTimeImmutable('1 year ago'); | |
// Now $entity->getCreationDate() returns '1 year ago'. Your fixture is ready! | |
} | |
} |
It’s obvious that this kind of checks makes tests less precise, flaky and subject to failures in slow machines or environments with fluctuating computational resources (like in most CI environments).
As you are probably thinking, these solutions are pretty far from being ideal. Let’s be honest: they just suck. So, one day we decided we needed to make our codebase a better place and we started looking for alternatives.
As with every refactoring, we started with the lowest hanging fruit (in terms of complexity). We tried Carbon, a great library for handling dates that also allows mocking the current date used when you create Carbon|CarbonImmutable
objects. In simple words, you can just do a Carbon::setTestNow($now)
to freeze the current time (or use the stateless counterpart Carbon::withTestNow($now, $closure)
).
As you would expect, our codebase needed some changes, and here they are:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Entity; | |
use Carbon\CarbonImmutable; | |
class Entity | |
{ | |
private CarbonImmutable $creationDate; | |
private ?CarbonImmutable $processingDate = null; | |
public function __construct() | |
{ | |
$this->creationDate = new CarbonImmutable(); | |
} | |
public function getCreationDate(): CarbonImmutable | |
{ | |
return $this->creationDate; | |
} | |
public function getProcessingDate(): ?CarbonImmutable | |
{ | |
return $this->processingDate; | |
} | |
public function scheduleNextProcessing(CarbonImmutable $processingDate): void | |
{ | |
$this->processingDate = $processingDate; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Service; | |
use DateTimeImmutable; | |
use Exception; | |
use Carbon\CarbonImmutable; | |
use App\Entity\Entity; | |
class Service | |
{ | |
public function doSomething(): void | |
{ | |
// NOTE: here we're still using DateTimeImmutable on purpose, to simulate a case in which it's not | |
// possible to refactor to use Carbon (e.g. 3rd party code out of our control or that cannot be | |
// refactored for any reason). | |
$now = new DateTimeImmutable(); | |
if ((int) $now->format('d') > 28) { | |
throw new Exception('This thing cannot be done after the 28th of every month.'); | |
} | |
// ... Something is done here | |
} | |
public function scheduleProcessing(Entity $entity): void | |
{ | |
$entity->scheduleNextProcessing(new CarbonImmutable('+5 minutes')); | |
} | |
} |
NOTE: I did not modify the use of DateTimeImmutable in Service::doSomething
method, because I wanted to simulate use of 3rd party code outside of our control that thus cannot be changed to use Carbon.
After the codebase refactoring, we are immediately able to improve some of our scenarios:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
use Carbon\CarbonImmutable; | |
class SomeIntegrationTest extends TestCase | |
{ | |
public function test_something_related_to_entity_creation_date() | |
{ | |
$entity = CarbonImmutable::withTestNow( | |
$oneYearAgo = new CarbonImmutable('1 year ago'), | |
fn () => new Entity() | |
); | |
// Now $entity->getCreationDate() returns '1 year ago'. Your fixture is ready! | |
} | |
} |
Because we decided we could not use Carbon in Service::doSomething
, we still cannot test that scenario.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use Carbon\Carbon; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
use App\Service\Service; | |
class ServiceTest extends TestCase | |
{ | |
public function test_it_schedules_processing_in_5_minutes_from_now() | |
{ | |
$service = new Service(); | |
// NOTE: not using a mock here just to keep things simple: you | |
// should probably use one in a unit test like this. | |
$entity = new Entity(); | |
Carbon::withTestNow( | |
$now = new CarbonImmutable(), | |
fn () => $service->scheduleProcessing($entity); | |
); | |
$this->assertEquals($now->modify('+5 minutes'), $service->getProcessingDate()); | |
} | |
} |
Except the refactoring (that takes time, even though Carbon should pretty much be a drop-in replacement of DateTime), it was very easy to configure as it’s just a library you install with Composer.
There are a couple of drawbacks with this approach, though:
- Any system function or class/method (e.g.
date()
,time()
,DateTime
, …) will still use the actual system clock, so it’s impossible to force code outside of your control to use your mocked date and time. - Your “business” code (entities and services) will have a hard dependency against
Carbon
(instead of sticking with the standard DateTime). This is not necessarily an issue, but it becomes relevant if you, like us, strive to keep exposure of your business logic to 3rd party libraries to the minimum.
Due to the above limitations, at some point we decided we wanted to improve our situation and we came across ext/timecop. This is basically a fork of its most famous Ruby counterpart, and thus is based on the same concept: mock the system clock via a PHP runtime extension, so that all running code uses it transparently (regardless of the library).
It’s simple: you invoke \timecop_freeze($date)
in your test code to bring the system clock to that specific moment. You then use \timecop_return()
(still in your test code) when you want to go back to the actual system clock.
NOTE: from here on, we go back to referring to the initial codebase, the one without Carbon.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
class SomeIntegrationTest extends TestCase | |
{ | |
public function test_something_related_to_entity_creation_date() | |
{ | |
\timecop_freeze(new DateTimeImmutable('1 year ago')); | |
$entity = new Entity(); | |
\timecop_return(); | |
// Now $entity->getCreationDate() returns '1 year ago'. Your fixture is ready! | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use Exception; | |
use PHPUnit\Framework\TestCase; | |
use App\Service\Service; | |
class ServiceTest extends TestCase | |
{ | |
public function test_something_cannot_be_done_after_28th_of_the_month() | |
{ | |
$service = new Service(); | |
$this->expectException(Exception::class); | |
\timecop_freeze(new DateTimeImmutable('Jan 29, 2022')); | |
$service->doSomething(); | |
// NOTE: you should make sure to call timecop_return() somewhere, e.g. a test listener | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
use App\Service\Service; | |
class ServiceTest extends TestCase | |
{ | |
public function test_it_schedules_processing_in_5_minutes_from_now() | |
{ | |
$service = new Service(); | |
// NOTE: not using a mock here just to keep things simple: you | |
// should probably use one in a unit test like this. | |
$entity = new Entity(); | |
\timecop_freeze($now = new DateTimeImmutable()); | |
$service->scheduleProcessing($entity); | |
\timecop_return(); | |
$this->assertEquals($now->modify('+5 minutes'), $service->getProcessingDate()); | |
} | |
} |
Great! We can go back to using stdlib, uninstall Carbon, and still test everything.
The price you have to pay, is that you need to be able to compile and install a php extension yourself. This might not be possible when you don’t have full control on your environment, like in shared hosting spaces -even though I hope you’re not running production applications there!
Anyway, we were happy and life was good. At least, until the day to update to PHP 8.0 came. That day, we found out that ext/timecop would NOT compile anymore.
The day got even worse when, after going to the GitHub repo and looking at the issue tracker, we realized that the project was abandoned.
We really liked the concept behind timecop, but we were forced to move on if we didn’t want to be stuck with PHP 7.4 forever. As writing and maintaining a PHP extension was outside of our possibilities, we had to find a different solution.
One day, I stumbled on a not-so-popular PHP extension: ext/uopz. It allows to modify, at runtime, implementations of methods and functions, including the ones of the standard library. Kudos to its author Joe Watkins and contributors for the amazing job!
ClockMock does mainly 2 things whenever mocks are activated by the developer:
- Overwrites implementation of many date and time-related functions with ones that the current time can be manipulated.
- Overwrites implementation of
\DateTime
and\DateTimeImmutable
with the ones of mock classes that consider the aforementioned mockable clock.
These implementations are reverted to their original versions whenever mocks are deactivated, so that there are no unintended side effects after using it.
DISCLAIMER: due to its “experimental” nature, we recommend to never use ClockMock in production.
You can use ClockMock with two different APIs:
- a stateful one, in which you need to balance inline calls to activate and reset mocks (like timecop)
- a stateless one, that will execute a closure at a specific point in time. It does not have side effects, as the original clock is always automatically restored.
Let’s see how the 3 scenarios can be rewritten with ClockMock:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
use SlopeIt\ClockMock\ClockMock; | |
class SomeIntegrationTest extends TestCase | |
{ | |
public function test_something_related_to_entity_creation_date() | |
{ | |
$entity = ClockMock::executeAtFrozenDateTime( | |
new DateTimeImmutable('1 year ago'), | |
fn () => new Entity() | |
); | |
// Now $entity->getCreationDate() returns '1 year ago'. Your fixture is ready! | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use Exception; | |
use PHPUnit\Framework\TestCase; | |
use App\Service\Service; | |
use SlopeIt\ClockMock\ClockMock; | |
class ServiceTest extends TestCase | |
{ | |
public function test_something_cannot_be_done_after_28th_of_the_month() | |
{ | |
$service = new Service(); | |
$this->expectException(Exception::class); | |
ClockMock::executeAtFrozenDateTime( | |
new DateTimeImmutable('Jan 29, 2022') | |
fn () => $service->doSomething() | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
use DateTimeImmutable; | |
use PHPUnit\Framework\TestCase; | |
use App\Entities\Entity; | |
use App\Service\Service; | |
use SlopeIt\ClockMock\ClockMock; | |
class ServiceTest extends TestCase | |
{ | |
public function test_it_schedules_processing_in_5_minutes_from_now() | |
{ | |
$service = new Service(); | |
// NOTE: not using a mock here just to keep things simple: you | |
// should probably use one in a unit test like this. | |
$entity = new Entity(); | |
ClockMock::executeAtFrozenDateTime( | |
$now = new DateTimeImmutable(), | |
fn () => $service->scheduleProcessing($entity); | |
); | |
$this->assertEquals($now->modify('+5 minutes'), $service->getProcessingDate()); | |
} | |
} |
You might be thinking that a more “standard” way of solving this problem would be to inject a ClockInterface
service that provides a way to obtain the current time (I also learned there’s a proposed standard for it). This way, the current time would be mockable like any other service.
This is definitely a valid approach. Problem is, in rather complex projects (not necessarily legacy) the current time is going to be needed in a wide range of places/classes. Furthermore, sometimes you may even need to get a \DateTime
or a timestamp from a 3rd party library and that would “escape” your ClockInterface
service (in case it’s impossible to make it interoperate with said library).
While it’s definitely doable to inject a Clock service in anything you manage using a DI container, it’s a lot less pragmatic to do the same for your value objects and/or entities.
When you need to create \DateTime
objects right inside entities (in constructor or mutator methods) it would be cumbersome having to pass a Clock from the outside every time. Sometimes, use of dates could even be a private implementation detail — having to pass a clock service from the outside would leak this detail unnecessarily and make the public interface more complex.
In other words, the reason why we built this library is that we think it’s a good tradeoff to consider the current system time as a “global state”, and we prefer to avoid injecting a service to access it.
For this reason, I think that this injection-based approach only makes sense when you have valid reasons for doing it besides testing (e.g. very time-sensitive applications for which you need control over subtle details, like monotonic wall time, leap second smearing, etc…).
Our application is not time sensitive, we only needed a way to mock the system clock in tests. If your needs are the same and you can afford to use ClockMock, you can start using it with zero changes to production code.
As you can see from the examples above, using ClockMock feels pretty much the same as using timecop (with a bonus: the syntactic sugar of executeAtFrozenDateTime
). It works with PHP 8 already, and the good thing is that the uopz extension is well maintained, so ClockMock is here to stay. We encourage you to use it and report any issues or feedback you may have.
The library is still incomplete, as mocks for some functions are missing. Just let us know if you want to help, contributions are welcome!
You can find the library on GitHub.
21