Могу ли я "издеваться" над временем в PHPUnit?
... не зная, подходит ли слово "издеваться".
В любом случае, у меня есть унаследованная база кода, для которой я пытаюсь написать некоторые тесты, основанные на времени. Стараясь не быть слишком расплывчатым, код связан с изучением истории элемента и определением, установил ли этот элемент пороговое значение времени.
В какой-то момент мне также нужно проверить, добавив что-то в эту историю, и проверить, что порог теперь изменен (и, очевидно, правильный).
В проблема, с которой я сталкиваюсь, заключается в том, что часть кода, который я тестирую, использует вызовы time(), и поэтому мне очень трудно точно определить, каким должно быть пороговое время, основываясь на том факте, что я не совсем уверен, когда именно будет вызвана функция time().
Итак, мой вопрос в основном таков: есть ли у меня какой-то способ "переопределить" вызов time() или каким-то образом "имитировать" время, чтобы мои тесты работали в "известное время"?
Или мне просто нужно принять тот факт, что мне придется что-то сделать в коде, который я тестирую, чтобы каким-то образом позволить мне заставить его использовать определенное время, если потребуется?
В любом случае, существуют ли какие-либо "общие методы" для разработки функциональных возможностей, зависящих от времени, которые удобны для тестирования?
Редактировать: Частью моей проблемы также является тот факт, что время, в течение которого происходили события в истории, влияет на порог. Вот пример части моей проблемы...
Представьте, что у вас есть банан, и вы пытаетесь потренироваться когда его нужно съесть. Допустим, срок его действия истечет в течение 3 дней, если только он не был распылен каким-либо химическим веществом, и в этом случае мы добавим 4 дня к истечению срока действия с момента нанесения спрея . Затем мы можем добавить к нему еще 3 месяца, заморозив его, но если он был заморожен, то у нас есть только 1 день, чтобы использовать его после того, как он оттает.
Все эти правила продиктованы историческим временем. Я согласен, что мог бы воспользоваться предложением Доминика о тестировании в течение нескольких секунд, но что из мои исторические данные? Должен ли я просто "создать" это на лету?
Как вы можете или не можете сказать, я все еще пытаюсь разобраться во всей этой концепции "тестирования";)
10 answers
Недавно я придумал другое решение, которое отлично подходит, если вы используете пространства имен PHP 5.3. Вы можете реализовать новую функцию time() в текущем пространстве имен и создать общий ресурс, в котором вы зададите возвращаемое значение в своих тестах. Тогда любой неквалифицированный вызов time() будет использовать вашу новую функцию.
Отказ от ответственности: Я написал эту библиотеку.
Вы можете имитировать время для теста, используя Часы из узо-лакомства.
В коде используйте просто:
$time = Clock::now();
Затем в тестах:
Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);
Лично я продолжаю использовать time() в тестируемых функциях/методах. В своем тестовом коде просто убедитесь, что вы проверяете не на равенство со временем(), а просто на разницу во времени менее 1 или 2 (в зависимости от того, сколько времени требуется для выполнения функции)
Мне пришлось имитировать конкретный запрос в будущем и в прошлую дату в самом приложении (не в модульных тестах). Следовательно, все вызовы \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
В большинстве случаев этого будет достаточно. У него есть некоторые преимущества:
- тебе не нужно ни над чем издеваться
- вам не нужны внешние плагины
- вы можете использовать любую функцию времени, не только time(), но и объекты DateTime
- вам не нужно использовать пространства имен.
Он использует phpunit, но вы можете добавить его в любую другую платформу тестирования, вам просто нужна функция, которая работает как assertContains() из phpunit.
1) Добавьте функцию ниже в свой тестовый класс или бутстрэп. Допуск по умолчанию для времени составляет 2 секунды. Вы можете изменить его, передав 3-й аргумент в assertTimeEquals или изменив аргументы функции.
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.
Этот тест должен быть пройден для правильной работы функции, ЕСЛИ выполнение тестовой функции занимает менее 2 секунд.
Это не идеально и не сверхточно, но это очень просто, и во многих случаях этого достаточно для тестирования что вы хотите проверить.
Для тех из вас, кто работает с symfony (>=2.8): Мост PHPUnit Symfony включает функцию синхронизации, которая переопределяет встроенные методы time
, microtime
, sleep
и usleep
.
См.: http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking
Самым простым решением было бы переопределить функцию PHP time() и заменить ее своей собственной версией. Однако вы не можете легко заменить встроенные функции PHP ( см. Здесь ).
Короче говоря, единственный способ - это абстрагировать вызов time() к какому-либо собственному классу/функции, который вернет время, необходимое для тестирования.
В качестве альтернативы вы можете запустить тестовую систему (операционную систему) на виртуальной машине и изменить время всего виртуального компьютера.
Вы можете переопределить функцию php time(), используя расширение runkit. Убедитесь, что вы установили runkit.internal_overide в значение On
Использование расширения [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();
}
Вот дополнение к сообщению фаба. Я сделал переопределение на основе пространства имен, используя оценку. Таким образом, я могу просто запустить его для тестов, а не для остальной части моего кода. Я запускаю функцию, аналогичную:
function timeOverrides($namespaces = array()) {
$returnTime = time();
foreach ($namespaces as $namespace) {
eval("namespace $namespace; function time() { return $returnTime; }");
}
}
Затем передайте timeOverrides(array(...))
в настройках теста, чтобы мои тесты должны были отслеживать только то, в каких пространствах имен вызывается time().