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

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

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

Это я не хочу, если можно:

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

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

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

5 ответов


вы должны заглушить методы 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 is вызванный. Затем можно заменить класс DateTime пользовательским классом DateTime с фиксированным временем. Другой вариант-использовать http://antecedent.github.io/patchwork


вы также можете использовать Time traveler lib, который использует расширение AOP php pecl, чтобы принести вещи, похожие на Ruby monkey patching https://github.com/rezzza/TimeTraveler

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


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

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

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

<?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()
        );
    }
}

вы можете изменить свою реализацию на instantiate 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());
    }
}

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

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

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

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

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

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
    }
}