W pewnym projekcie operuję poprzez API na liście wydań produktu: zamykam wydania, których czas już minął, i tworzę nowe na osiem tygodni naprzód. Moimi punktami odniesienia w czasie są:

  • new \DateTime – dziś,
  • new \DateTime('+8 weeks') – koniec okresu, w którym tworzę wydania,
  • new \DateTime('last friday') – bo cykl każdego wydania kończy się w piątek, od tej daty iteruję +1 week tworząc nowe wydania.

Dwie sprawy komplikują nam życie, jeśli chcemy stworzyć testy jednostkowe do tej klasy: po pierwsze nie powinniśmy naprawdę odwoływać się do API, a jedynie udać zapytania, a po drugie powinniśmy udać że dzisiaj jest któryśtam, jakaś stała data, do której będą dostosowane nasze przypadki testowe.

Musimy zatem stworzyć parę dubli – klas, które “przysłonią” swoje pierwowzory, podmieniając niektóre ich metody na swoje. Świetnym narzędziem do tego celu jest biblioteka AspectMock, potrafiąca stworzyć takie duble, które wszystkim innym nawet się nie śniły (np. duble metod statycznych czy funkcji systemowych).

Niestety, choć AspectMock potrafi “nadpisać” funkcję time(), na nic to się zda, jeśli używamy DateTime (a kto nie używa?), a ją nadpisać jest już ciężej. Na szczęście da się to zrobić za pomocą pewnej sztuczki:

<?php
namespace VR\Redmine;

class VersionsHandler
{
    /** @var Api */
    private $api;

    private $now;

    private $until;

    public function __construct(Api $api)
    {
        $this->api = $api;

        $this->now = TimeProvider::getDateTimeFormatted();

        $this->until = TimeProvider::getDateTimeFormatted('+8 weeks');
    }

    // (...)

    private function getToBeCreated()
    {
        $current = TimeProvider::getDateTime('last friday');
        // (...)
    }

Jak widać, we wszystkich miejscach, gdzie normalnie użyłbym DateTime, ja korzystam z klasy TimeProvider. Jest ona bardzo prościutka:

<?php
namespace VR\Redmine;

class TimeProvider
{
    public static function getDateTime($time = 'now')
    {
        return new \DateTime($time);
    }

    public static function getDateTimeFormatted($time = 'now')
    {
        $datetime = self::getDateTime($time);

        return $datetime->format('Y.\WW');
    }
}

I istnieje tylko po to, by móc ją zdublować w następujący sposób:

$timeProxy = Test::double('VR\\Redmine\\TimeProvider', ['getDateTime' => function($time) {
    $dt = new \DateTime('2015-01-14');
    return $dt->modify($time);
}]);

A zatem podczas normalnego uruchomienia aplikacji, TimeProvider działa po prostu jak nicnierobiąca fabryka DateTime’ów. W czasie testów jednak obiera ona za punkt odniesienia arbitralną datę 2015-01-04, a potem modyfikuje ją o to, co podamy jej w parametrze, jak jakby dziś był 2015-01-04. Zawsze, niezależnie kiedy uruchomimy test.

Teraz wystarczy tylko zdublować funkcję odwołującą się do API i przeprowadzić testy:

$apiProxy = Test::double(
  'VR\\Redmine\\Api',
  ['curl' => function($url, $method = 'GET', $data = []) {
    if ($method != 'GET') { return null; }

    return json_encode(/* mocked api response */);
}]);

$handler = new VersionsHandler(new Api);

$this->assertEquals(/* expected output */, $handler->handle());