Могу ли я "Mock" время в PHPUnit?

... не зная, является ли "макет" правильным словом.

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

В какой-то момент мне также нужно попробовать добавить что-то в эту историю и проверить, что порог теперь изменен (и, очевидно, исправлен).

Проблема, с которой я сталкиваюсь, заключается в том, что часть кода, который я тестирую, использует вызовы времени(), и поэтому мне очень трудно точно узнать, какое пороговое время должно быть, на основании факта что я не совсем уверен, когда будет вызвана функция time().

Итак, мой вопрос в основном таков: есть ли способ для меня "переопределить" вызов time() или каким-то образом "измотать" время, чтобы мои тесты работали в "известное время"?

Или мне просто нужно признать тот факт, что мне придется что-то делать в коде, который я тестирую, чтобы каким-то образом позволить мне заставить его использовать определенное время, если это необходимо?

В любом случае существуют ли какие-либо "общие практики" для разработки чувствительных к времени функциональных возможностей, дружественных к тестированию?

Изменить: Часть моей проблемы также заключается в том, что время, которое произошло в истории, влияет на порог. Вот пример части моей проблемы...

Представьте, что у вас есть банан, и вы пытаетесь разобраться, когда его нужно съесть. Скажем, что срок его действия истекает в течение 3 дней, если только он не был распылен каким-либо химическим веществом, и в этом случае мы добавляем 4 дня до истечения срока действия с момента применения спрея. Затем мы можем добавить еще 3 месяца к нему, заморозив его, но если он был заморожен, у нас есть только 1 день, чтобы использовать его после его оттаивания.

Все эти правила продиктованы историческими таймингами. Я согласен с тем, что в течение нескольких секунд я мог бы использовать предложение Dominik по тестированию, но что из моих исторических данных? Должен ли я просто "создавать" это на лету?

Как вы можете или не можете сказать, я все еще пытаюсь понять всю концепцию "тестирования";)

Ответ 1

Недавно я придумал другое решение, которое отлично, если вы используете пространства имен PHP 5.3. Вы можете реализовать новую функцию time() внутри вашего текущего пространства имен и создать общий ресурс, в котором вы устанавливаете возвращаемое значение в своих тестах. Тогда любой неквалифицированный вызов функции time() будет использовать вашу новую функцию.

Для дальнейшего чтения я подробно описал его в блоге

Ответ 2

Отказ от ответственности: я написал эту библиотеку.

Вы можете высмеять время для тестирования, используя Clock из ouzo-goodies.

В коде просто используйте:

$time = Clock::now();

Затем в тестах:

Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);

Ответ 3

Лично я продолжаю использовать time() в проверенных функциях/методах. В своем тестовом коде просто убедитесь, что вы не проверяете равенство по времени(), а просто для разницы во времени менее 1 или 2 (в зависимости от того, сколько времени функция выполняет для выполнения)

Ответ 4

Мне пришлось моделировать конкретный запрос в будущем и прошлую дату в самом приложении (не в модульных тестах). Следовательно, все вызовы \DateTime:: now() должны возвращать дату, установленную ранее в приложении.

Я решил пойти с этой библиотекой https://github.com/rezzza/TimeTraveler, так как я могу высмеять даты без изменения всех кодов.

\Rezzza\TimeTraveler::enable();
\Rezzza\TimeTraveler::moveTo('2011-06-10 11:00:00');

var_dump(new \DateTime());           // 2011-06-10 11:00:00
var_dump(new \DateTime('+2 hours')); // 2011-06-10 13:00:00

Ответ 5

В большинстве случаев это будет сделано. Он имеет ряд преимуществ:

  • вам не нужно ничего издеваться над
  • вам не нужны внешние плагины
  • вы можете использовать любую функцию времени, а не только time(), но и объекты DateTime
  • вам не нужно использовать пространства имен.

Он использует phpunit, но вы можете добавить его в любую другую структуру тестирования, вам просто нужна функция, которая работает как assertContains() из phpunit.

1) Добавьте функцию ниже в ваш тестовый класс или загрузочный файл. Допуск по умолчанию для времени составляет 2 секунды. Вы можете изменить его, передав третий аргумент assertTimeEquals или изменив функции args.

private function assertTimeEquals($testedTime, $shouldBeTime, $timeTolerance = 2)
{
    $toleranceRange = range($shouldBeTime, $shouldBeTime+$timeTolerance);
    return $this->assertContains($testedTime, $toleranceRange);
}

2) Пример тестирования:

public function testGetLastLogDateInSecondsAgo()
{
    // given
    $date = new DateTime();
    $date->modify('-189 seconds');

    // when
    $this->setLastLogDate($date);

    // then
    $this->assertTimeEquals(189, $this->userData->getLastLogDateInSecondsAgo());
}

assertTimeEquals() проверяет, содержит ли массив из (189, 190, 191) 189.

Этот тест должен быть передан для правильной рабочей функции. IF, выполняющий тестовую функцию, занимает менее 2 секунд.

Это не идеально и суперточно, но это очень просто и во многих случаях достаточно проверить, что вы хотите проверить.

Ответ 6

Простейшим решением было бы переопределить функцию PHP time() и заменить ее собственной версией. Однако вы не можете легко заменить встроенные функции PHP (см. Здесь).

Короче говоря, единственный способ - абстрагировать вызов time() на какой-либо собственный класс/функцию, которая вернет время, необходимое для тестирования.

В качестве альтернативы вы можете запустить тестовую систему (операционную систему) на виртуальной машине и изменить время всего виртуального компьютера.

Ответ 7

Вы можете переопределить функцию php time() с помощью расширения runkit. Убедитесь, что вы установили runkit.internal_overide в On

Ответ 8

Использование расширения [runkit] [1]:

define('MOCK_DATE', '2014-01-08');
define('MOCK_TIME', '17:30:00');
define('MOCK_DATETIME', MOCK_DATE.' '.MOCK_TIME);

private function mockDate()
{
    runkit_function_rename('date', 'date_real');
    runkit_function_add('date','$format="Y-m-d H:i:s", $timestamp=NULL', '$ts = $timestamp ? $timestamp : strtotime(MOCK_DATETIME); return date_real($format, $ts);');
}


private function unmockDate()
{
    runkit_function_remove('date');
    runkit_function_rename('date_real', 'date');
}

Вы можете даже проверить макет, как это:

public function testMockDate()
{
    $this->mockDate();
    $this->assertEquals(MOCK_DATE, date('Y-m-d'));
    $this->assertEquals(MOCK_TIME, date('H:i:s'));
    $this->assertEquals(MOCK_DATETIME, date());
    $this->unmockDate();
}

Ответ 9

Здесь добавление к публикации. Я использовал переопределение пространства имен с использованием eval. Таким образом, я могу просто запустить его для тестирования, а не для остальной части моего кода. Я запускаю функцию, похожую на:

function timeOverrides($namespaces = array()) {
  $returnTime = time();
  foreach ($namespaces as $namespace) {
    eval("namespace $namespace; function time() { return $returnTime; }");
  }
}

затем передайте timeOverrides(array(...)) в тестовой настройке, чтобы мои тесты только отслеживали, на что вызывается имя пространства time().

Ответ 10

Для тех из вас, кто работает с symfony ( >= 2.8): Symfony PHPUnit Bridge включает функцию ClockMock, которая переопределяет встроенные методы time, microtime, sleep и usleep.

Смотрите: http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking