Mocking The Time, используемое всеми экземплярами DateTime для целей тестирования.

Я хотел бы указать время для каждого экземпляра DateTime, созданного на время PHPUnit или Behat Test.

Я тестирую бизнес-логику, относящуюся ко времени. Например, метод в классе возвращает события только в прошлом или в будущем.

Вещь, которую я не хочу делать, если это возможно:

1) Напишите обертку вокруг DateTime и используйте это вместо DateTime на протяжении всего моего кода. Это потребует повторной записи моей текущей базы кода.

2) Динамически генерировать набор данных каждый раз, когда выполняется тест/пакет.

Итак, вопрос: возможно ли переопределить поведение DateTimes, чтобы всегда задавать определенное время по запросу?

Ответы

Ответ 1

Вы должны заглушить методы DateTime, которые вам нужны в тестах, чтобы вернуть ожидаемые значения.

    $stub = $this->getMock('DateTime');
    $stub->expects($this->any())
         ->method('theMethodYouNeedToReturnACertainValue')
         ->will($this->returnValue('your certain value'));

См. http://www.phpunit.de/manual/3.6/en/test-doubles.html

Если вы не можете заглушить методы, потому что они жестко закодированы в ваш код, посмотрите

который объясняет, как вызывать обратный вызов всякий раз, когда вызывается new. Затем вы можете заменить класс DateTime на пользовательский класс DateTime с фиксированным временем. Другим вариантом было бы использовать http://antecedent.github.io/patchwork

Ответ 2

Вы также можете использовать библиотеку времени путешественника, которая использует расширение aop php pecl, чтобы принести вещи, похожие на рубиновое обезглавливание обезьян https://github.com/rezzza/TimeTraveler

Там также это расширение php, вдохновленное рубином timecop one: https://github.com/hnw/php-timecop

Ответ 3

Добавляя к тому, что уже сказал @Gordon, есть один, довольно хакерский способ проверки кода, который зависит от текущего времени:

Мое издевательствование только одного защищенного метода, который дает вам "глобальное" значение, которое вы можете обойти проблемы, связанные с необходимостью создания класса самостоятельно, чтобы вы могли запрашивать такие вещи, как текущее время (которое было бы чище, но в php it является спорным/понятным, что люди не хотят этого делать).

Это будет выглядеть примерно так:

<?php

class Calendar {

    public function getCurrentTimeAsISO() {
        return $this->currentTime()->format('Y-m-d H:i:s');
    }

    protected function currentTime() {
        return new DateTime();
    }

}
?>

<?php


class CalendarTest extends PHPUnit_Framework_TestCase {

    public function testCurrentDate() {
        $cal = $this->getMockBuilder('Calendar')
            ->setMethods(array('currentTime'))
            ->getMock();
        $cal->expects($this->once())
            ->method('currentTime')
            ->will($this->returnValue(
                new DateTime('2011-01-01 12:00:00')
            )
        );
        $this->assertSame(
            '2011-01-01 12:00:00',
            $cal->getCurrentTimeAsISO()
        );
    }
}

Ответ 4

Вы можете изменить свою реализацию, чтобы явно создать DateTime() с помощью time():

new \DateTime("@".time());

Это не изменяет поведение вашего класса. Но теперь вы можете mock time() предоставить функцию с именами:

namespace foo;
function time() {
    return 123;
}

Вы также можете использовать мой пакет php-mock/php-mock-phpunit для этого:

namespace foo;

use phpmock\phpunit\PHPMock;

class DateTimeTest extends \PHPUnit_Framework_TestCase
{

    use PHPMock;

    public function testDateTime()
    {
        $time = $this->getFunctionMock(__NAMESPACE__, "time");
        $time->expects($this->once())->willReturn(123);

        $dateTime = new \DateTime("@".time());
        $this->assertEquals(123, $dateTime->getTimestamp());
    }
}

Ответ 5

Поскольку я использую Symfony WebTestCase для выполнения функционального тестирования с использованием пакета тестирования PHPUnit, быстро стало непрактично издеваться над всеми обычаями DateTime класс.

Я хотел протестировать приложение, поскольку он обрабатывает запросы с течением времени, такие как тестирование cookie или истечения срока действия кэша и т.д.

Лучший способ, который я нашел для этого, - реализовать собственный класс DateTime, который расширяет класс по умолчанию, и предоставляет некоторые статические методы, позволяющие добавлять/вычитать по умолчанию временные перекосы ко всем объектам DateTime, созданным из этот пункт далее.

Это очень простая функция для реализации и не требует установки пользовательских библиотек.

caveat emptor: Единственный недостаток этого метода заключается в том, что инфраструктура Symfony (или любая используемая вами среда) не будет использовать вашу библиотеку, поэтому любое поведение, которое, как он ожидал, будет обрабатывать самостоятельно, например внутренних кеш файлов/файлов cookie, эти изменения не будут затронуты.

namespace My\AppBundle\Util;

/**
 * Class DateTime
 *
 * Allows unit-testing of DateTime dependent functions
 */
class DateTime extends \DateTime
{
    /** @var \DateInterval|null */
    private static $defaultTimeOffset;

    public function __construct($time = 'now', \DateTimeZone $timezone = null)
    {
        parent::__construct($time, $timezone);
        if (self::$defaultTimeOffset && $this->isRelativeTime($time)) {
            $this->modify(self::$defaultTimeOffset);
        }
    }

    /**
     * Determines whether to apply the default time offset
     *
     * @param string $time
     * @return bool
     */
    public function isRelativeTime($time)
    {
        if($time === 'now') {
            //important, otherwise we get infinite recursion
            return true;
        }
        $base = new \DateTime('2000-01-01T01:01:01+00:00');
        $base->modify($time);
        $test = new \DateTime('2001-01-01T01:01:01+00:00');
        $test->modify($time);

        return ($base->format('c') !== $test->format('c'));
    }

    /**
     * Apply a time modification to all future calls to create a DateTime instance relative to the current time
     * This method does not have any effect on existing DateTime objects already created.
     *
     * @param string $modify
     */
    public static function setDefaultTimeOffset($modify)
    {
        self::$defaultTimeOffset = $modify ?: null;
    }

    /**
     * @return int the unix timestamp, number of seconds since the Epoch (Jan 1st 1970, 00:00:00)
     */
    public static function getUnixTime()
    {
        return (int)(new self)->format('U');
    }

}

Использование этого простого:

public class myTestClass() {
    public function testMockingDateTimeObject()
    {
        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "before: ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "before: ". (new DateTime())->format('c') . "\n";

        DateTime::setDefaultTimeOffset('+25 hours');

        echo "fixed:  ". (new DateTime('18th June 2016'))->format('c') . "\n";
        echo "after:  ". (new DateTime('tomorrow'))->format('c') . "\n";
        echo "after:  ". (new DateTime())->format('c') . "\n";

        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // before: 2016-09-20T00:00:00+00:00
        // before: 2016-09-19T11:59:17+00:00
        // fixed:  2016-06-18T00:00:00+00:00 <-- stayed same
        // after:  2016-09-21T01:00:00+00:00 <-- added 25 hours
        // after:  2016-09-20T12:59:17+00:00 <-- added 25 hours
    }
}