Mocking Время, используемое всеми экземплярами DateTime для тестирования - PullRequest
19 голосов
/ 31 октября 2011

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

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

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

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

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

Таким образом, вопрос заключается в следующем: возможно ли переопределить DateTimeПоведение s всегда предоставлять определенное время по запросу?

Ответы [ 5 ]

20 голосов
/ 31 октября 2011

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

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

См. https://phpunit.de/manual/current/en/test-doubles.html

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

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

6 голосов
/ 24 мая 2014

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

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

2 голосов
/ 02 ноября 2011

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

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

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

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

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

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()
        );
    }
}
1 голос
/ 19 сентября 2016

Поскольку я использую 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
    }
}
1 голос
/ 22 апреля 2016

Вы можете изменить свою реализацию, чтобы явно создавать 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());
    }
}
...