Did you ever write unit tests for code that dealt with date and time? Since time itself is out of your control, you might have used one of these approaches:
- Test doubles, e.g. for the DateTime class in PHP. To make this work, DateTime or a factory thereof must be passed to the subject under test via dependency injection. My most popular blog post of all time is still How to mock time() in PHPUnit about mocking core functions like date() and time() if necessary (a hacky workaround, mind you)
- Use derived values of the current time in fixtures and assertions, probably with some margin, e.g.
assertEquals(\strtotime(+1 day), $cache->expiresAt(), 1)
where$cache->expiresAt()
is the value we test and 1 is the allowed margin.
Luckily, there is a third way, namely using a custom Clock (or Calendar) object that provides the current time and can easily be replaced with an (also custom) test double.
Here is a minimal interface for such a Clock object:
interface Clock
{
public function timestamp(): int;
}
You may add methods to convert from and to date strings if you need them, but with this minimal interface you already have all you need if you do not need time in microseconds. From there, we can derive other date and time functions, e.g. the built-in functions date() and strtotime() can take an optional argument with the current timestamp.
The real implementation is just as simple:
class SystemClock implements Clock
{
public function timestamp(): int
{
return \time();
}
}
Using the dependency injection framework of your choice, you can now depend on the Clock interface and pass a SystemClock instance by default.
For example, in Magento, SystemClock would be configured as preference for Clock, which works for any class that takes Clock as a constructor argument.
Usage example - a class that formats the current date:
class Today
{
/**
* @var Clock
*/
private $clock;
public function __construct(Clock $clock)
{
$this->clock = $clock;
}
public function toString(): string
{
return date('Y-m-d', $this->clock->timestamp());
}
}
The test double
For tests, we create a custom fake implementation instead of using a mocking framework. First, we need to make the current timestamp configurable:
class FakeClock implements Clock
{
private $timestamp;
public function __construct(int $timestamp)
{
$this->timestamp = $timestamp;
}
public function set(int $timestamp)
{
$this->timestamp = $timestamp;
}
public function timestamp(): int
{
return $this->timestamp;
}
}
In a test, we instantiate and pass the fake clock:
$today = new Today(new FakeClock(0));
$this->assertEquals('1970-01-01', $today->toString(), 'Today should be the beginning of the UNIX epoch');
The fake clock above is nothing more than a stub that returns a configured value. But if treated carefully, it can evolve in a powerful tool: Every time you would call set()
in your tests, think about the purpose and add a semantic method instead. For example, if you want to simulate time passing by, you can create a method like this:
public function advance(string $interval)
{
$this->timestamp = strtotime($interval, $this->timestamp);
return $this;
}
And your test can turn from
$clock = new FakeClock(strtotime('1996-07-01 12:00:00')
// ...
$clock->set(strtotime('1996-07-02 12:00:00')
to
$clock = new FakeClock(strtotime('1996-07-01 12:00:00')
// ..
$clock->advance('+ 1 day');
which makes the intent immediately clear.
How these additional methods look like may depend heavily on your domain and the code under test. Do not hesitate to write very specific methods; a generic FakeClock class would not make much sense anyways. You would lose the benefit of explicit semantics in tests and gain the burden of another unnecessary dependency.
Some examples that may or may not be useful for you:
public function tick()
{
$this->timestamp++;
}
public function toNextDay()
{
$this->timestamp = strtotime("tomorrow 0:00", $this->timestamp);
}
public function rightBefore(int $timestamp)
{
$this->timestamp = $timestamp - 1;
}
public function rightAfter(int $timestamp)
{
$this->timestamp = $timestamp + 1;
}
Using clock objects instead of built-ins like time
and DateTime
directly makes the dependency on an external recource (time) explicit. It does not mean that we should not use DateTime
anymore, but if used for the current time, instead of
$now = new DateTime
it should be
$now = new DateTime(‘@' . $clock->timestamp);
or add a convenient factory method to Clock and use it like this:
$now = $clock->createDateTime();
Summary
Custom Clock objects allows us to better deal with date and time in tests. They don't make the client code overly complicated, give us full control, and with the right semantic methods in the fake implementation, make our tests much more expressive than it would be possible with a standard mock.