Time is an integral part of our lives — the same for the applications we develop. From the software engineering perspective, time is just another dependency which we introduce to our systems. And that's OK. The situation might start to be problematic when some parts of the system are more dependent on time. How to test this component? How to mock the time?
In this article, I'm going to show you our approach to develop and test components which depend on time. We'll get rid of time-dependency by creating a proper abstraction and applying the dependency inversion principle. Ultimately, we'll create a mechanism to mock time in tests. Ready?
The problem
Time is continuous — its value is changing all the time. Basically, it's impossible to reach the same application state, when it depends on time. Let's consider this example:
public function generateName(File $file): string
{
return $file->getName() . "_" . time();
}
The implementation of the above method is not testable since we can’t control the output from the time()
function. Using the time()
function to construct expected string also doesn’t solve the problem. The outcome may change between successive executions. Let’s consider more complex example:
class OrderFilter
{
private const EXPIRATION_TIME = 2592000; // 30 days
public function filterObsolete(array $orders): array
{
return array_filter($orders, function (Order $order) {
return $order->getUpdatedAt() < time() - self::EXPIRATION_TIME;
});
}
}
This code reflects business rule — the order should get obsolete if is no updated for 30 days. To perform this filtering, the service needs to have knowledge about the current date and time.
In order to test the presented service, we would need to create a bunch of objects whose updatedAt
property will change in each consecutive execution. If we decide to hardcode each value, after 3 months, scenarios will start to fail.
I told that time is nothing more than another dependency. But how we can deal with it? Is time something that we can e.g. inject into our services? Yes, if we use a proper abstraction.
Introduce Clock
If you look at the clock, you get to know what time is it. Perfect! Let's define a simple interface for the clock.
interface ClockInterface
{
public function currentTime(): \DateTimeImmutable;
public function currentTimestamp(): int;
}
Do you need more? Feel free to extend this interface. For our case, it’s enough. Let me propose a simple implementation.
final class Clock implements ClockInterface
{
public function currentTime(): \DateTimeImmutable
{
return new \DateTimeImmutable('now');
}
public function currentTimestamp(): int
{
return time();
}
}
Now, we have a decent abstraction behind time with a proper implementation. Let’s go back to the previous example and replace global time
function with our new service.
class OrderFilter
{
private const EXPIRATION_TIME = 2592000; // 30 days
public function filterObsolete(array $orders): array
{
$clock = new Clock();
return array_filter($orders, function (Order $order) use ($clock) {
return $order->getUpdatedAt() < $clock->currentTime() - self::EXPIRATION_TIME;
});
}
}
Yes, I did it literally, but it might be even better. The implementation detail — Clock — is clearly visible now, but the code breaks Dependency Inversion Principle. We need to fix it.
Remove low-level dependency
According to Robert C. Martin’s definition of the Dependency Inversion Principle:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Our service depends on the low-level Clock
service instead of the abstraction. We can fix this issue by taking advantage of Dependency Injection.
class OrderFilter
{
private const EXPIRATION_TIME = 2592000; // 30 days
private $clock;
public function __construct(ClockInterface $clock)
{
$this->clock = $clock;
}
public function filterObsolete(array $orders): array
{
return array_filter($orders, function (Order $order) {
return $order->getUpdatedAt() < $this->clock->currentTime() - self::EXPIRATION_TIME;
});
}
}
We may configure which implementation our application should use when ClockInterface
is needed. But the real advantage is the possibility to test various cases without worrying about the time.
class OrderFilterTest extends TestCase
{
public function testShouldReturnOnlyObsoleteOrders()
{
$clock = $this->createMock(ClockInterface::class);
$clock->method('currentTime')->willReturn(1551544373); // 2019-03-02 16:32
$orders = $this->createWithUpdatedTime([
1551441600, // 2019-03-01 12:00
1543672800, // 2018-12-01 14:00 (obsolete)
1543572300, // 2018-11-30 10:05 (obsolete)
]);
$filter = new OrderFilter($clock);
$results = $filter->filterObsolete($orders);
static::assertCount(2, $results);
}
// ...
}
Mocking time in functional tests
Unit tests are responsible for checking a single unit of code, e.g. class, whereas functional tests are responsible for checking the correctness of business requirements against specific components. How we can prepare a universal test case if we cannot substitute ClockInterface
directly?
This is an example test scenario from our system, which checks the correctness of the invoicing process. It's written as CodeceptionCest scenario (simplified version)
/**
* @property IntegrationTester $tester
*/
class RunMonthlyInvoicingCommandCest
{
public function runMonthlyInvoicingSuccessfully(IntegrationTester $I)
{
$users = $I->haveUsers(2);
$I->haveOrdersBelongingToUsers($users, $ordersCount = 2);
$I->amCollectingDomainEvents(InvoiceBooked::class);
$I->pushCommandToTheBus(new RunMonthlyInvoicingCommand());
$I->seeThatDomainEventOccurExactly(InvoiceBooked::class, 4);
}
}
Under the hood, this process uses the current time to perform necessary filters and checks. Unfortunately, on different days we've received different outcomes. To make the whole process replayable, we have to run it with the same conditions. What about time?
We've added a custom Codeception helper, which adds extra method. We put it in necessary scenarios before the actual test case.
$I->assumeThatTodayIs('last day of this month');
The string passed as an argument to this function is basically the same as the argument to the DateTime
object constructor. Behind the scenes, Codeception replaces the definition of ClockInterface
in dependency injection container by fixed clock implementation. What the FixedClock
really is? The clock which returns the same preconfigured time.
The Codeception helper for Yii2 Framework may look as follows:
class Time extends \Codeception\Module
{
public function assumeThatTodayIs($date)
{
\Yii::$container->setSingleton(ClockInterface::class, function () use ($date) {
if (is_string($date)) {
$date = new \DateTimeImmutable($date);
}
return new FixedClock($date);
});
}
}
The actual implementation of this helper may be different in a different framework. For Symfony, we would define another service only for test environment. On the Codeception level, the code would set the time directly to the FixedClock
implementation.
As you can see, by hiding time dependency behind the dedicated abstraction, we get more flexibility when it comes to tests. Our code is cleaner and easier to maintain.
Summary
We had been looking for a solution which would have the smallest impact on our existing codebase. Some tools were well-known for us, e.g. libraries like Carbon. We found also a different and richer implementation of the clock in PHP. However, we didn't want to add another library to our project to cover this specific problem.
Aspect-oriented Programming (AOP) offers a perfect solution for this problem defining special hooks before execution of specific function or method. It's a powerful tool, which doesn't require any modification of existing code. Moreover, it covers all third-party code as well. Unfortunately, it requires introducing aspects – another, sometimes magical way of defining extra behavior, what can be hard at the beginning.
We thought also about utilizing namespaces a bit to redeclare ‘time’ function. The problem was, that only time
was problematic, but also objects like DateTime
or DateTimeImmutable
.
Ultimately, we decided to implement the simplest solution based on Dependency Injection. This technique is well-known in OOP and it shows intentions explicitly, what is definitely plus.
Does it mean, that we refactored each occurrence of time
or DateTime
in our system? Of course, no. It wasn’t necessary. We created a mechanism, that let us release our components from dependency on time, making tests useful again.