- PVSM.RU - https://www.pvsm.ru -
Вы получили или пришли на проект, которому d+дцать лет? PHP код был написан в перерывах между охотой на мамонтов и поэтому слегка не читаем? Вам предстоит это как минимум сапортить, как максимум — рефакторить или переписывать?
Если у вас после этих вопросов не участилось дыхание или пульс — проходите мимо, эта статья для тех, кто уже бывал жертвой таких издевательств или предчувствует такой поворот судьбы.
Речь пойдет об одной конкретной задаче, типичной для этой ситуации — покрытии юнит тестами legacy-кода перед его рефактором или изменением. А именно — создание заглушек (моканье, симулирование, etc) для функций и/или методов «на лету».
Хочу предложить решения для следующих двух, как по мне — основных, проблем:
public function getSomething($param1, $param2)
{
$result1 = mysql_query('SELECT * FROM table1');
// ...
if ($result1['field'] == $param1) {
$result2 = mysql_query('SELECT * FROM table2');
}
// ...
if ($result2['field'] == $param2) {
$result3 = mysql_query('SELECT * FROM table3');
}
// ...
return isset($result3) ? $result3 : $result2;
}
Чтобы покрыть тестом такой код — есть несколько вариантов:
public function sendSomething(array $data)
{
$ch = curl_init();
$result = mysql_query('SELECT url FROM info WHERE id = ' . $data['someId']);
curl_setopt($ch, CURLOPT_URL, $result['url']);
curl_setopt($ch, CURLOPT_POSTFIELDS, implode('&', $data);
// ...
curl_exec($ch);
}
public function myMethod()
{
$data = SomeCLass::getSomeData();
// ...
$data = OtherClass::modifyData($data);
// ...
// еще сотня-другая кода, влияющего на содержание массива $data
// ...
$this->sendSomething($data);
// ...
return $completelyOtherVariable;
}
Варианты:
Примеры, в основном, «притянуты за уши», но в той или иной степени схожести, по крайней мере в моей практике, такие ситуации встречаются. Да и так нагляднее.
Скорее всего, наиболее безболезненно все это пройдет если выбрать вариант #3 в обоих случаях. Нужно только определиться, что использовать, runkit или uopz? Для меня ответ очевиден потому, что писать php-код в строку и передавать его как параметр — извращение.
Основная функция, которую мы используем, но не нативно:
void uopz_function ( string $class , string $function , Closure $handler [, int $modifiers ] )
Она предельно проста. Мы сообщаем данные функции, которую собираемся переопределить и передаем анонимную функцию, которая будет выполнена вместо исходной. Так же там можно «поиграть» с областью видимости функции, но сейчас не об этом.
На этом можно было бы остановиться, потому что любой middle+ программист уже примерно понял, что делать дальше, а junior-у вряд ли поручат такую задачу ввиду высокой вероятности суицида.
Эта статья предназначена только лишь немного ускорить работу каторжника и сделать его код чуть более читабельным и коротким.
Поэтому, хочу предложить вам 2 вещи:
Дублировать весь код я не буду, просто оставлю здесь [1] ссылку на gist. И для удобства кратко перечислю его методы.
uopzFlags($function, $flags); // изменяет флаги
uopzRedefine($constant, $value); // переопределяет константу
uopzFunction($function, Closure $closure, $backup = false); // аналог "чистой" uopz_function за исключением того, что умеет backup-ить и принимать имя функции или метода: 'mysql_query' или ['ClassName', 'methodName']
uopzMuteFunction($function, $backup = false); // просто блокирует выполнение чего-либо, например, если вы не хотите, чтобы какой-то метод отправил письмо при ошибке, или curl не "дергал" url, etc
uopzRestore($function); // восстановление функции из backup-а
uopzBackup($function); // backup функции/метода (удобнее это делать при переопределении)
uopzFunctionSimpleReturn($function, $return, $backup = false); // простая подмена возвращаемого значения. return может быть скаляром, объектом (будет возвращен клон) или анонимной функцией.
uopzFunctionReplace($function, $replace, $backup = false); // замена одной функции другой.
uopzFunctionConsistentReturn($function, array $return, $backup = false); // последовательная замена возвращаемого значения. Нужна в тех случаях, когда точно известна последовательность вызова. Например, если функция вызывается в цикле.
uopzFunctionConditionReturn($function, array $conditionList, $default = null, $backup = false); // возврат значения по условию. Условие состоит из названия аргумента вызываемой функции и его значения.
uopzFunctionHook($function, Closure $closure, &$return, $backup = false); // перехват функции и возврат значения по ссылке.
Ну, и, собственно, решение тех двух проблем с помощью «этого»:
$this->uopzFunctionConsistentReturn('mysql_query', [
['id' => 12, 'data' => 'dummy'],
['id' => 31, 'data' => 'dummy'],
['id' => 45, 'data' => 'dummy'],
]);
// Или, второй способ, с помощью условий (здесь он избыточен, конечно):
$this->uopzFunctionConditionReturn('mysql_query', [
['query', 'SELECT * FROM table1', ['id' => 12, 'data' => 'dummy']],
['query', 'SELECT * FROM table2', ['id' => 31, 'data' => 'dummy']],
['query', 'SELECT * FROM table3', ['id' => 45, 'data' => 'dummy']],
]);
$this->uopzFunctionHook(
['ClassName', 'sendSomething'],
function() { return $data; }, // просто возвращаем полученный параметр
$data // сюда по ссылке мы получим то, что из myMethod передается в sendSomething как $data
);
Мне это сэкономило огромную кучу времени, поэтому — решил поделиться. Надеюсь, кому-то это тоже станет полезным. И еще больше надеюсь, что в мире с каждым днем будет становится все меньше такого кода, где это будет полезно :)
Спасибо за внимание.
Автор: jced
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/213650
Ссылки в тексте:
[1] здесь: https://gist.github.com/jced-artem/b38f861b0912885e14001acf78c33847
[2] Источник: https://habrahabr.ru/post/316140/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.