Testing complex time problems

Time problems are hard.  Testing them shouldn't have to be.  Recently I had a set of requirements that came with a set of complex time related conditions.  There were a total of 27 different outcomes based on a set of inputs, the current time being one of them.

The architecture of the Drupal project can help make testing time easier.  Time itself is a service that can be simplified and put to work for the developer.

Controlling Time

To override the time service, a Service Provider can be used.

<?php

namespace Drupal\some_module;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Overrides the time service with a custom implementation.
 */
class SomeModuleServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    if ($container->hasDefinition('datetime.time')) {
      $definition = $container->getDefinition('datetime.time');
      $definition->setClass('Drupal\some_module\Time');
    }
  }
}

The new service can redefine how time works.  For example, the normally dynamic nature of time be brought to a complete stop.  It can also be conveniently controlled.

<?php

namespace Drupal\some_module;

use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Overrides the core time service and allows for time to be manipulated.
 */
class Time extends \Drupal\Component\Datetime\Time {

  use MessengerTrait;
  use StringTranslationTrait;

  /**
   * {@inheritdoc}
   */
  public function getCurrentTime() {
    $current_date = parent::getCurrentTime();
    $query = $this->requestStack->getCurrentRequest()->query;
    if ($query->has('current_date')) {
      $override_str = $query->get('current_date');
      if ($override = strtotime($override_str)) {
        $current_date = $override;
        $this->messenger()->addStatus('The current time has been overridden');
      }
      else {
        $this->messenger()->addWarning('"@date" does not match the required format, YYYY-MM-DD.', ['@date' => $override_str]);
      }
    }
    return $current_date;
  }

}

With this service in place, the current date can be easily manipulated via a query parameter.  Of course, this isn't something something that one would want to do in production, but for automated functional tests this can be a lifesaver.