Comments 54
Это может говорить о том что ваш рекурсивный метод возвращает результат разных типов? Это неверно.
Альтернатива — создавать такие входные данные, которые бы позволяли протестировать все ветки рекурсии в одном тестовом методе.
Зачем? Создайте нужное количество методов чтобы протестировать все разновидности входных и выходных данных. Сложная логика в тестах тоже неверно.
Это может говорить о том что ваш рекурсивный метод возвращает результат разных типов? Это неверно.
Рекурсивный метод вызывает сам себя. Если я тестирую ветку кода, в которой вызывается этот же метод, то, по-хорошему, при втором заходе я должен использовать мок, а не вызывать сам себя. Но я не могу замокировать метод в процессе его выполнения.
Создайте нужное количество методов чтобы протестировать все разновидности входных и выходных данных
С рекурсией так не проходит. Если не создавать обертку. Попробуйте создать два тестовых метода (для обоих условий выхода) для простейшей рекусивной функции:
function factorial($x)
{
if ($x === 0) {
return 1;
} else {
return $x * factorial($x - 1);
}
}
Сложная логика в тестах тоже неверно.
Абсолютно согласен.
Не знаю что там такого сложного может быть внутри рекурсивной функции, но если она реально настолько сложная, лучше перепишите её через итерацию.
С рекурсией так не проходит. Если не создавать обертку. Попробуйте создать два тестовых метода (для обоих условий выхода) для простейшей рекусивной функции:
Я бы тупо написал 4 простых теста:
для -1 (и мы бы выяснили что ваша функция получает переполнение стека на этом тесте)
для 0, чтобы результат был равен 1
для 1, чтобы результат был равен 1
для 5, чтобы убедиться что результат равен 120
И никакие моки тут вообще не нужны. Соответственно для более сложных рекурсивных функций (построение меню бесконечной вложенности или обход деревьев и графов) я бы сделал вспомогательную функцию билдер которая из какого-нибудь простого типа данных строит эту сложную структуру и также подавал бы на вход понятный список данных и ожидал бы такого же понятного ответа. Тоже без моков.
Итерации, как правило, сложнее рекурсии в написании и восприятии. А создание сложных входных структур противоречит нами обоими одобренному тезису "Сложная логика в тестах тоже неверно." Но если вы можете сделать тестирование рекурсии простым и без моков — делайте простым и без моков.
Почему вообще вы считаете, что рекурсивные методы надо тестировать как-то иначе, чем остальные? То, рекурсивный метод, или нет — его личное дело, ваше дело протестировать, что на определенные входы он отдает ожидаемые выходы.
Почему вы считаете, что я считаю, что "рекурсивные методы надо тестировать как-то иначе, чем остальные"? Я наоборот считаю, что рекурсивные методы нужно тестировать точно так же, как и остальные. И если нужно использовать при тестах моки — то нужно использовать моки.
Так моки нужно использовать только тогда, когда без них не обойтись. Для рекурсивных методов это часто бывает?
Это как-то противоречит тому, что я написал?
Нет, я не мокаю циклы, хотя с удовольствием посмотрел бы на пример. Я мокаю именно зависимости. Рекурсивная функция становится зависимой от самой себя в тот момент, когда обращается сама к себе. Или я и зависимость неправильно понимаю?
А то с виду получается, что вы тестите свою имплементацию какого-то алгоритма (именно сами строчки кода), а не его поведение. Не уверен что в понятие 100% покрытия кода входит именно это.
Мне казалось, что "зависимость" более широкое определение, чем "аргумент функции", и не зависит от нашего желания или нежелания проверять что-либо в тесте. Ну да ладно. Да, я тестирую конкретную имплементацию определенного алгоритма, разбивая, при необходимости, ее на составляющие и проверяя каждую составляющую в отдельности. Разве не в этом, в декомпозиции сложной системы на более простые составляющие, заключается основной подход в уменьшении сложности?
Рекурсивная функция не может быть проще, чем она же. Поэтому мокая рекурсивный вызов вы не получаете упрощения — зато получаете излишнюю зависимость от реализации.
Скажем, те же факториалы можно считать рекурсивно, а можно — итеративно. Какая мне разница, как именно (через рекурсию или нет) они считаются, если намного проще проверить, что функция правильно их считает?
Рекурсивная функция не может быть проще, чем она же. Поэтому мокая рекурсивный вызов вы не получаете упрощения
Для простых случаев, типа расчета факториалов, это справедливо, но если рекурсивная функция достаточно сложная, то получаю. Вот, например, преобразование ассоциативного массива в объект заданного типа:
public function parseArrayData($type, $data)
{
$isArray = $this->_toolType->isArray($type);
$typeNorm = $this->_toolType->normalizeType($type);
$typeData = $this->_typePropsRegistry->register($typeNorm);
if ($isArray) {
/* process $data as array of $types */
$result = [];
foreach ($data as $key => $item) {
$result[$key] = $this->parseArrayData($typeNorm, $item);
}
} else {
/* process $data as data object of $type */
$result = $this->_manObj->create($typeNorm);
foreach ($data as $key => $value) {
$propName = $this->_toolType->formatPropertyName($key);
if (isset($typeData[$propName])) {
$propertyData = $typeData[$propName];
$propertyType = $propertyData->getType();
$propertyIsArray = $propertyData->getIsArray();
if ($propertyIsArray) {
/* property is the array of types */
$propertyType = $this->_toolType->getTypeAsArrayOfTypes($propertyType);
$complex = $this->parseArrayData($propertyType, $value);
$result->setData($propName, $complex);
} else {
if ($this->_toolType->isSimple($propertyType)) {
/* property is the simple type */
$result->setData($propName, $value);
} else {
/* property is the complex type, we need to convert recursively */
$complex = $this->parseArrayData($propertyType, $value);
$result->setData($propName, $complex);
}
}
}
}
}
return $result;
}
Может с алгоритмом что-то не так? Я тут вижу как-то много дублирования.
А можете точнее указать, где именно вы видите дублирование? Было бы хорошо, если бы вы представили свою версию метода, без лишнего дублирования. Это бы сразу придало вес вашим словам. Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (properties) могут быть простыми (строка, число), сложными (другой объект с иерархической структурой) или массивом простых или сложных объектов. Метод небольшой, укладывается с один экран, если убрать лишнее дублирование — получится еще меньше. Не думаю, что это займет у вас много времени.
Да. сложные объекты (complex type) все имеют метод setData($property, $value). Но можно и по-другому, например setProperty($value). Перегоняется JSON, приходящий на API-сервис в объект, содержащий данные (аналог java beans). Был при признателен за пример универсальной фабрики или строителя для создания подобных объектов, код которого бы не выходил за рамки экрана.
Спасибо за пример. Насколько я понял, итоговый код фабрик/строителей для 10-15 объектов выйдет за пределы одного экрана. Более того, с увеличением кол-ва объектов, используемых в API, кол-во фабрик/строителей (как следствие — кол-во кода) также будет расти. В моем случае парсер один и для одного объекта, и для сотни, как и регистратор _typePropsRegistry, который анализирует через рефлексию заданный тип и формирует массив доступных для инициализации свойств.
Статья о рекурсии, а не о конвертации ассоциативного массива в объект. Я выложил пример рекурсии, а вы предложили сделать "по-другому". Я поинтересовался, можно ли сделать универсальную фабрику/строитель и получил ответ на свой вопрос. Если вас действительно интересует, как в _typePropsRegistry реализована обработка опциональных и обязательных полей и все остальное — то можете глянуть. Это не мой код, но я брал его за основу, т.к. этот не поддерживает объявление методов через аннотации.
ну и еще. рефлексия какбы довольно тяжелый механизм
Как бы да. Но на общем фоне обработки HTTP-запроса уже как бы и нет.
Или я и зависимость неправильно понимаю?
Рекурсивная функция — она одна. То есть она по определению не может быть зависимостью самой себе. Рекурсивный вызов — это деталь реализации функции. Внутри вместо рекурсии в один прекрасный день можно поиском вширину заняться или вообще в циклик развернуть все. И вот эти изменения не должны приводить к правкам тестов.
По сути что вы сделали в тестах — это продублировали реализацию используя моки как "описание" оной. Это категорически не практично особенно при тестировании функций.
Вместо этого стоило бы подумать, "а не написал ли я что-то не то"? Возможно вы выбрали неудобные структуры данных, возможно еще что-то…
Рекурсивные функции — их легко тестировать. Подаем что-то на вход и ожидаем что-то на выходе. Никаких моков не нужно для этого.
Реку́рсия — определение, описание, изображение какого-либо объекта или процесса внутри самого этого объекта или процесса, то есть ситуация, когда объект является частью самого себя.
Это вопрос формулировок. Я считаю что объект зависит от частей, из которых состоит, вы — что нет.
Рекурсивные функции — их легко тестировать. Подаем что-то на вход и ожидаем что-то на выходе. Никаких моков не нужно для этого.
Я не настаиваю на использовании моков при тестировании рекурсии. Более того, я не настаиваю на использовании рекурсии, или использовании моков, или вообще тестирования. Я просто полагаю, что некоторые рекурсивные функции могут быть несколько сложнее, чем вычисление факториала, могут использовать внешние зависимости со сложным поведением и при необходимости (или даже возможности) тестирования таких функций можно использовать мокирование этой же самой функции.
Вы настаиваете на том, что при тестировании рекурсивных функций мокирование категорически неприемлемо?
Это вопрос формулировок. Я считаю что объект зависит от частей, из которых состоит, вы — что нет.
Тогда он зависит и от своих полей, и от своих методов… Нет, зависимость — это всегда внешняя по отношению к объекту сущность.
Как вам будет угодно считать. Вот пример агрегации из wiki:
class Ehe // Пример агрегации
{
private:
Person& _partner1; // Enthaltener Teil. // Aggregation
Person& _partner2; // Enthaltener Teil. // Aggregation
public:
// Конструктор
Ehe (Person& partner1, Person& partner2)
: _partner1(partner1), _partner2(partner2)
{ }
};
если я не ошибаюсь, то агрегация — это зависимость, а _partner1 — это поле.
Вот только зависимость — она не от поля, а от экземпляра Person
, который является внешним по отношению к объекту (что хорошо видно в конструкторе). Поле — это всего лишь форма его хранения.
Можете считать, что объект не зависит от своих составляющих.
Объект зависит от сторонних типов и только. Он по сути зависит от реализации интов в вашем языке программирования (привет bc_math) но с такого рода "зависимостями" мы обычно спокойно миримся и даже не обращаем внимания.
Объект зависит от объектов, составляющих его или которые он использует. Но объект не зависит от своих методов. Методы могут тянуть за собой зависимости от сторонних типов, свойства могут содержать значения с каким-то типом… но вот функция имеющая одну и ту же сингатуру (рекурсия ж) не может быть зависимостью от самой себя. Во всяком случае не в контексте тестирования ибо в этом ровным счетом нет никакого смысла.
Окей. Зависимость — это все что дефайнется ВНЕ модуля. То есть то что мы импортируем в use например. То что дефайнится внутри модуля не может быть зависимостью. Нутро модуля может использовать что-то, что задефайнено извне (типы например), но это не структурные элементы являются зависимостями, а конкретные значения, их типы и все такое.
То есть повторюсь. Функция не может быть "зависимостью" самой себя. А сталобыть "мокать" ее — бредовая идея. И проблема в вашем случае не с рекурсией а с высокой цикломатической сложностью вашей реализации. В теории ее можно разделить на отдельные функции и тогда уже тестировать будет сильно проще.
Зависимость — это все что дефайнется ВНЕ модуля.
Я уже отметил выше, что вашу точку зрения понял и принял к сведению :)
"мокать" ее — бредовая идея
Как говорится "В теории, теория и практика неразделимы. На практике это не так" (с) Вы вчера на мой пример рекурсии сказали примерно следующее
Может с алгоритмом что-то не так? Я тут вижу как-то много дублирования.
Если вы приведете более простой пример этого же алгоритма, то рассмотрим его, если же более простого примера не будет, то попробуйте прикинуть входные данные для того, чтобы протестировать все ветки этого алгоритма из одного тестового метода. В этой функции 4 "цикла" либо вы "мокаете" все зависимости на глубину в 4 цикла, либо вы декомпозируете ваш тест на 4 части и "мокаете" все зависимости, включая вызов самой себя, только на один проход. Вот и прикиньте теперь, где цикломатическая сложность будет выше.
Если вы приведете более простой пример этого же алгоритма
вы привели его кусок. Так дела не делаются. Либо опишите решаемую задачу, либо никак.
Я же дал описание задачи сразу же под вашим комментом:
Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (properties) могут быть простыми (строка, число), сложными (другой объект с иерархической структурой) или массивом простых или сложных объектов.
Чуть ниже есть еще пояснение по вопросам возникшим у нашего коллеги:
сложные объекты (complex type) все имеют метод setData($property, $value). Но можно и по-другому, например setProperty($value). Перегоняется JSON, приходящий на API-сервис в объект, содержащий данные (аналог java beans).
Если что-то в описании непонятно — с удовольствием отвечу на вопросы.
Спасибо за ответ, коллега. Когда мне нужен будет пример поверхностности и/или догматизма — я буду о вас вспоминать.
Допускаю, что лучший пример рекурсии для вас — вычисление факториала. И могу полностью с вами согласиться, что на "лучшем примере рекурсии" нет необходимости мокать собственно саму рекурсивную функцию, там нет никаких зависимостей и всего-то два прохода — первый и последний. Но в моем примере таких проходов 4 и плюс пара зависимостей, чье поведение нужно также замокать на соответствующую глубину. И если вы мне укажите, в каком месте моего примера нарушен благословенный SRP, то я буду премного вам благодарен и пересмотрю свои взгляды как минимум для данного примера. Пока же "нарушение SRP" в примере настолько же доказанный факт, как и "дублирование".
Входные данные — это ассоциативный массив и имя класса в который этот ассоциативный массив должен быть преобразован. Не вижу ничего страшного в том, что входные данные могут быть разных типов. А результат у меня один — объект. SRP не ограничивает кол-во и тип входных/выходных данных, насколько мне известно.
принцип единственной обязанности (англ. Single responsibility principle) обозначает, что каждый объект должен иметь одну обязанность и эта обязанность должна быть полностью инкапсулирована в класс. Все его сервисы должны быть направлены исключительно на обеспечение этой обязанности.
Какую еще обязанность вы обнаружили в моем примере, кроме преобразования ассоциативного массива в объект заданного типа?
Как я говорил, преобразование только в объекты, аналогичные java-beans (вот определение). Там нет ничего, кроме акцессоров. Добавление новых типов объектов и новых свойств в существующие типы не влияет на существующий код. Никакого god object'а я не вижу — тупой функционал.
А вот сакрализация рекурсии — любимое развлечение computer science.
Т.е. в реальной жизни рекурсия используется только там, где она реально нужна.
Другое дело, что 99% задач программировния в «реальной жизни» — это получить данные, провести над ними несложные преобразования, отдать данные дальше.
Тестирование рекурсии