PHP + AspectMock – fałszowanie czasu

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());
A photo of me

About the author

Hi! I'm Andrea (they/them). I tell computers what to do, both for a living and for fun, I'm also into blogging, writing and photography. I'm trying to make the world just a little bit better: more inclusive, more rational and more just.

Related posts:

Avris FunctionMock

FunctionMock is a simple and elegant way to mock away system/global functions in your tests.

Continue reading…
(~2 min read)
gitlab.com/Avris/FunctionMock
100% coverage - feels good!

Having 100% of LOC covered by unit tests certainly feels like a great achievement. But beware – that doesn’t necessarily mean your code is perfectly covered. Lines of code coverage is a really nice indicator of your app’s stability, but is can also hide some risks.

Continue reading…
(~6 min read)