Могу ли я "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