Есть пара весомых поводов не использовать рекурсию, но это не повод не использовать рекурсию вообще. Программы, во-первых, создаются программистами для программистов, и лишь во-вторых — программистами для компьютеров. В итоге, некоторыми годными программами могут пользоваться неподготовленные люди. Рекурсия имеет одно безусловное преимущество перед итерацией — читабельность. Когда программист создает программы для себе подобных, рекурсия имеет право на существование до тех пор, пока не докажет обратного (т.е. — не будет запущена на компьютере и не поперхнется реальными данными).
Тестирование — это, по сути, создание программ для программ, позволяющее программистам отодвигать порог непреодолимой сложности в разрабатываемых приложениях. Столкнувшись на днях с необходимостью написать юнит-тест для рекурсивного метода я был неприятно удивлен необходимостью мокировать сам тестируемый метод. Альтернатива — создавать такие входные данные, которые бы позволяли протестировать все ветки рекурсии в одном тестовом методе. В перспективе вырисовывалось не снижение сложности, а наоборот — ее увеличение. Порывшись в интернетах, я обнаружил кучу информации о том, чем нехороша рекурсия, массу советов, как перейти от рекурсии к итерации, но так и не нашел на русских формах того, что искал — как тестировать рекурсивный метод. Решив, что подготовить тестовые данные для трех проходов по коду — не такая уж непреодолимая сложность, отложил эту задачу до утра. Под катом решение, пришедшее в голову за ночь, позволяющее разбивать тестирование рекурсивных методов на части.
Рекурсивный метод
public function foo($arg1, $arg2)
{
//...
$out = $this->foo($in1, $in2);
//...
}
Рекурсивный метод с оберткой
Создаем обертку для метода и делаем так, чтобы сам метод вызывал только обертку, а обертка вызывала метод:
public function foo($arg1, $arg2)
{
//...
$out = $this->fooRecursive($in1, $in2);
//...
}
public function fooRecursive($arg1, $arg2)
{
return $this->foo($arg1, $arg2);
}
Мокирование "обертки"
public function test_foo()
{
/* create mock for wrapper 'fooRecursive'*/
$obj = \Mockery::mock(\FooClass::class . '[fooRecursive]');
$obj->shouldReceive('fooRecursive')->once()
->andReturn('out');
/* call recursive method 'foo' */
$res = $obj->foo('arg1', 'arg2');
}
Да, решение весьма незамысловатое, но, может кому-нибудь пригодится, и ему удастся потратить свою ночь на что-то более полезное.
Комментарии (54)
flancer
17.08.2016 10:32Это может говорить о том что ваш рекурсивный метод возвращает результат разных типов? Это неверно.
Рекурсивный метод вызывает сам себя. Если я тестирую ветку кода, в которой вызывается этот же метод, то, по-хорошему, при втором заходе я должен использовать мок, а не вызывать сам себя. Но я не могу замокировать метод в процессе его выполнения.
Создайте нужное количество методов чтобы протестировать все разновидности входных и выходных данных
С рекурсией так не проходит. Если не создавать обертку. Попробуйте создать два тестовых метода (для обоих условий выхода) для простейшей рекусивной функции:
function factorial($x) { if ($x === 0) { return 1; } else { return $x * factorial($x - 1); } }
Сложная логика в тестах тоже неверно.
Абсолютно согласен.
youlose
17.08.2016 12:06+3Рекурсивный метод вызывает сам себя. Если я тестирую ветку кода, в которой вызывается этот же метод, то, по-хорошему, при втором заходе я должен использовать мок, а не вызывать сам себя. Но я не могу замокировать метод в процессе его выполнения.
Не знаю что там такого сложного может быть внутри рекурсивной функции, но если она реально настолько сложная, лучше перепишите её через итерацию.
С рекурсией так не проходит. Если не создавать обертку. Попробуйте создать два тестовых метода (для обоих условий выхода) для простейшей рекусивной функции:
Я бы тупо написал 4 простых теста:
для -1 (и мы бы выяснили что ваша функция получает переполнение стека на этом тесте)
для 0, чтобы результат был равен 1
для 1, чтобы результат был равен 1
для 5, чтобы убедиться что результат равен 120
И никакие моки тут вообще не нужны. Соответственно для более сложных рекурсивных функций (построение меню бесконечной вложенности или обход деревьев и графов) я бы сделал вспомогательную функцию билдер которая из какого-нибудь простого типа данных строит эту сложную структуру и также подавал бы на вход понятный список данных и ожидал бы такого же понятного ответа. Тоже без моков.
flancer
17.08.2016 12:46Итерации, как правило, сложнее рекурсии в написании и восприятии. А создание сложных входных структур противоречит нами обоими одобренному тезису "Сложная логика в тестах тоже неверно." Но если вы можете сделать тестирование рекурсии простым и без моков — делайте простым и без моков.
lair
17.08.2016 13:28+1Почему вообще вы считаете, что рекурсивные методы надо тестировать как-то иначе, чем остальные? То, рекурсивный метод, или нет — его личное дело, ваше дело протестировать, что на определенные входы он отдает ожидаемые выходы.
flancer
17.08.2016 13:57Почему вы считаете, что я считаю, что "рекурсивные методы надо тестировать как-то иначе, чем остальные"? Я наоборот считаю, что рекурсивные методы нужно тестировать точно так же, как и остальные. И если нужно использовать при тестах моки — то нужно использовать моки.
lair
17.08.2016 14:11+1Так моки нужно использовать только тогда, когда без них не обойтись. Для рекурсивных методов это часто бывает?
flancer
17.08.2016 14:13Это как-то противоречит тому, что я написал?
youlose
17.08.2016 15:02+2Мне кажется что вы несколько неверно понимаете для чего нужно моки, моки нужны для имитации объектов-зависимостей со сложным поведением, но рекурсивная функция же не является зависимостью самой себя? Это же просто цикл, просто выражен иначе. Вы же циклы не мокаете?
flancer
17.08.2016 15:19Нет, я не мокаю циклы, хотя с удовольствием посмотрел бы на пример. Я мокаю именно зависимости. Рекурсивная функция становится зависимой от самой себя в тот момент, когда обращается сама к себе. Или я и зависимость неправильно понимаю?
youlose
17.08.2016 15:43Ну обычно зависимости, это то что передаётся в аргументах функции или если наш метод вызывает другой в пределах класса, но мы их проверять в этом тесте не хотим.
А то с виду получается, что вы тестите свою имплементацию какого-то алгоритма (именно сами строчки кода), а не его поведение. Не уверен что в понятие 100% покрытия кода входит именно это.flancer
17.08.2016 15:58Мне казалось, что "зависимость" более широкое определение, чем "аргумент функции", и не зависит от нашего желания или нежелания проверять что-либо в тесте. Ну да ладно. Да, я тестирую конкретную имплементацию определенного алгоритма, разбивая, при необходимости, ее на составляющие и проверяя каждую составляющую в отдельности. Разве не в этом, в декомпозиции сложной системы на более простые составляющие, заключается основной подход в уменьшении сложности?
lair
17.08.2016 16:35Рекурсивная функция не может быть проще, чем она же. Поэтому мокая рекурсивный вызов вы не получаете упрощения — зато получаете излишнюю зависимость от реализации.
Скажем, те же факториалы можно считать рекурсивно, а можно — итеративно. Какая мне разница, как именно (через рекурсию или нет) они считаются, если намного проще проверить, что функция правильно их считает?
flancer
17.08.2016 17:02Рекурсивная функция не может быть проще, чем она же. Поэтому мокая рекурсивный вызов вы не получаете упрощения
Для простых случаев, типа расчета факториалов, это справедливо, но если рекурсивная функция достаточно сложная, то получаю. Вот, например, преобразование ассоциативного массива в объект заданного типа:
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; }
Fesor
17.08.2016 23:04+2Может с алгоритмом что-то не так? Я тут вижу как-то много дублирования.
flancer
18.08.2016 10:33А можете точнее указать, где именно вы видите дублирование? Было бы хорошо, если бы вы представили свою версию метода, без лишнего дублирования. Это бы сразу придало вес вашим словам. Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (properties) могут быть простыми (строка, число), сложными (другой объект с иерархической структурой) или массивом простых или сложных объектов. Метод небольшой, укладывается с один экран, если убрать лишнее дублирование — получится еще меньше. Не думаю, что это займет у вас много времени.
oxidmod
18.08.2016 10:42+1я конечно хз, но меня почемуто смущает этот метод вообще.
получается что этот метод должен знать о всех классах которые он инициализирует.
все эти классы обязаны иметь setData и по другому никак
имхо. для создания сложных объектов лучше заюзать фабрику или строителя.flancer
18.08.2016 10:53Да. сложные объекты (complex type) все имеют метод setData($property, $value). Но можно и по-другому, например setProperty($value). Перегоняется JSON, приходящий на API-сервис в объект, содержащий данные (аналог java beans). Был при признателен за пример универсальной фабрики или строителя для создания подобных объектов, код которого бы не выходил за рамки экрана.
oxidmod
18.08.2016 11:46interface ObjectFactoryInterface
{
public function createObject(array $data);
}
class Obj1Factory implements ObjectFactoryInterface
{
public function createObject(array $data)
{
return (new Obj1())
->setProperty1($data['property1']??null)
...;
}
}
в контролере чтото типа такого:
$factory = $this->container->get('api.factories.'.$apiData['objectType']);
$obj = $factory->createObject($apiData['objectData']);
зы. сори, парсер чтото не парсит нормально
ззы. да, на каждый объект нужен свой строитель/фабрика
зззы. в кажый строитель/фабрику при помощи DI вы можете насетапать нужные зависимости, чтобы правильно построить итоговый объект
ззззы. если объект содержит другие объекты в полях, то нужен строительflancer
18.08.2016 19:16Спасибо за пример. Насколько я понял, итоговый код фабрик/строителей для 10-15 объектов выйдет за пределы одного экрана. Более того, с увеличением кол-ва объектов, используемых в API, кол-во фабрик/строителей (как следствие — кол-во кода) также будет расти. В моем случае парсер один и для одного объекта, и для сотни, как и регистратор _typePropsRegistry, который анализирует через рефлексию заданный тип и формирует массив доступных для инициализации свойств.
oxidmod
18.08.2016 19:43+1интересно, что код _typePropsRegistry вы не приложили) вместе с ним наверняка выйдет больше одного екрана.
второй момент, что я не представляю как этот _typePropsRegistry определяет какие поля обязательные, какие опциональные. и что делать, если поля надо сетить через конструктор, а не через сеттеры.
третий момент. ваш _typePropsRegistry будет становится все больше и сложней при увеличении количества объектов и способов их создания. я придерживаюсь мысли, что каждый должен делать чтото одно, оно же SRP.
да, у меня много классов, но каждый из них очень простой. Я в любой момент могу добавить еще одного строителя для любого нового типа объекта.
ну и еще. рефлексия какбы довольно тяжелый механизмflancer
19.08.2016 07:24Статья о рекурсии, а не о конвертации ассоциативного массива в объект. Я выложил пример рекурсии, а вы предложили сделать "по-другому". Я поинтересовался, можно ли сделать универсальную фабрику/строитель и получил ответ на свой вопрос. Если вас действительно интересует, как в _typePropsRegistry реализована обработка опциональных и обязательных полей и все остальное — то можете глянуть. Это не мой код, но я брал его за основу, т.к. этот не поддерживает объявление методов через аннотации.
ну и еще. рефлексия какбы довольно тяжелый механизм
Как бы да. Но на общем фоне обработки HTTP-запроса уже как бы и нет.
oxidmod
19.08.2016 09:17зависит от того, сколько разных объектов нужно создать.
>>универсальную фабрику/строитель
этим требованием вы нарушаете один из важнейших принципов хорошего дизайна, SRP.
Много кода — это не беда. Код пишется один раз и работает всегда.
Беда когда много кода сложного и сильно связанного. он становится очень неустойчивым к изменениям.
И в конкретном случае создания сложных объектов рекурсия неуместна, имхо. это плохой пример её применения
зы. глянул код)) таки да, с _typePropsRegistry размер сильно больше одного екрана. Тогда как одна простая фабрика вмешается на екран без проблем.
ззы. а еще оно зависит от NameFinder, котоырй думаю не меньше, сейчас глянем)
oxidmod
19.08.2016 09:24через конструктор не разглядел… я к примеру люблю когда все параметры задаются с конструктора, а сеттеров может и не быть, если по БЛ свойство не меняется напрямую. Притом оно может менятся неким другим методом, например $post->incrementViews();
не говорю уже о завязке на докблок… его банально может и не быть или не втом виде, в котором ожидаетсяflancer
19.08.2016 09:52Когда-то давным-давно я пытался писАть программы, которые работают при любых условиях. Сейчас мне достаточно, чтобы они отрабатывали при определенных. А что касается любимых приемов, то "на цвет и вкус все фломастеры разные" (с)
oxidmod
19.08.2016 10:05>> Когда-то давным-давно я пытался писАть программы, которые работают при любых условиях. Сейчас мне достаточно, чтобы они отрабатывали при определенных.
это лично ваш опыт, но не стоит выдавать его за best practice
Fesor
17.08.2016 23:10+2Или я и зависимость неправильно понимаю?
Рекурсивная функция — она одна. То есть она по определению не может быть зависимостью самой себе. Рекурсивный вызов — это деталь реализации функции. Внутри вместо рекурсии в один прекрасный день можно поиском вширину заняться или вообще в циклик развернуть все. И вот эти изменения не должны приводить к правкам тестов.
По сути что вы сделали в тестах — это продублировали реализацию используя моки как "описание" оной. Это категорически не практично особенно при тестировании функций.
Вместо этого стоило бы подумать, "а не написал ли я что-то не то"? Возможно вы выбрали неудобные структуры данных, возможно еще что-то…
Рекурсивные функции — их легко тестировать. Подаем что-то на вход и ожидаем что-то на выходе. Никаких моков не нужно для этого.
flancer
18.08.2016 10:45Реку?рсия — определение, описание, изображение какого-либо объекта или процесса внутри самого этого объекта или процесса, то есть ситуация, когда объект является частью самого себя.
Это вопрос формулировок. Я считаю что объект зависит от частей, из которых состоит, вы — что нет.
Рекурсивные функции — их легко тестировать. Подаем что-то на вход и ожидаем что-то на выходе. Никаких моков не нужно для этого.
Я не настаиваю на использовании моков при тестировании рекурсии. Более того, я не настаиваю на использовании рекурсии, или использовании моков, или вообще тестирования. Я просто полагаю, что некоторые рекурсивные функции могут быть несколько сложнее, чем вычисление факториала, могут использовать внешние зависимости со сложным поведением и при необходимости (или даже возможности) тестирования таких функций можно использовать мокирование этой же самой функции.
Вы настаиваете на том, что при тестировании рекурсивных функций мокирование категорически неприемлемо?
lair
18.08.2016 10:54+1Это вопрос формулировок. Я считаю что объект зависит от частей, из которых состоит, вы — что нет.
Тогда он зависит и от своих полей, и от своих методов… Нет, зависимость — это всегда внешняя по отношению к объекту сущность.
flancer
18.08.2016 11:30Как вам будет угодно считать. Вот пример агрегации из 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 — это поле.
lair
18.08.2016 11:43+1Вот только зависимость — она не от поля, а от экземпляра
Person
, который является внешним по отношению к объекту (что хорошо видно в конструкторе). Поле — это всего лишь форма его хранения.flancer
18.08.2016 19:02-2Можете считать, что объект не зависит от своих составляющих.
Fesor
18.08.2016 22:15+1Объект зависит от сторонних типов и только. Он по сути зависит от реализации интов в вашем языке программирования (привет bc_math) но с такого рода "зависимостями" мы обычно спокойно миримся и даже не обращаем внимания.
Объект зависит от объектов, составляющих его или которые он использует. Но объект не зависит от своих методов. Методы могут тянуть за собой зависимости от сторонних типов, свойства могут содержать значения с каким-то типом… но вот функция имеющая одну и ту же сингатуру (рекурсия ж) не может быть зависимостью от самой себя. Во всяком случае не в контексте тестирования ибо в этом ровным счетом нет никакого смысла.
flancer
19.08.2016 07:32-2Да, я уже понял, что вы с коллегой lair разделяете "зависимости" на "кошерные" и те, на которые "не обращаем внимания". Не беспокойтесь, для меня не составит труда учитывать этот факт при общении с вами. Как вы правильно заметили — всякому овощу свой контекст.
Fesor
19.08.2016 12:32+1Окей. Зависимость — это все что дефайнется ВНЕ модуля. То есть то что мы импортируем в use например. То что дефайнится внутри модуля не может быть зависимостью. Нутро модуля может использовать что-то, что задефайнено извне (типы например), но это не структурные элементы являются зависимостями, а конкретные значения, их типы и все такое.
То есть повторюсь. Функция не может быть "зависимостью" самой себя. А сталобыть "мокать" ее — бредовая идея. И проблема в вашем случае не с рекурсией а с высокой цикломатической сложностью вашей реализации. В теории ее можно разделить на отдельные функции и тогда уже тестировать будет сильно проще.
flancer
19.08.2016 13:03Зависимость — это все что дефайнется ВНЕ модуля.
Я уже отметил выше, что вашу точку зрения понял и принял к сведению :)
"мокать" ее — бредовая идея
Как говорится "В теории, теория и практика неразделимы. На практике это не так" (с) Вы вчера на мой пример рекурсии сказали примерно следующее
Может с алгоритмом что-то не так? Я тут вижу как-то много дублирования.
Если вы приведете более простой пример этого же алгоритма, то рассмотрим его, если же более простого примера не будет, то попробуйте прикинуть входные данные для того, чтобы протестировать все ветки этого алгоритма из одного тестового метода. В этой функции 4 "цикла" либо вы "мокаете" все зависимости на глубину в 4 цикла, либо вы декомпозируете ваш тест на 4 части и "мокаете" все зависимости, включая вызов самой себя, только на один проход. Вот и прикиньте теперь, где цикломатическая сложность будет выше.
Fesor
19.08.2016 16:38Если вы приведете более простой пример этого же алгоритма
вы привели его кусок. Так дела не делаются. Либо опишите решаемую задачу, либо никак.
flancer
19.08.2016 17:20Я же дал описание задачи сразу же под вашим комментом:
Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (properties) могут быть простыми (строка, число), сложными (другой объект с иерархической структурой) или массивом простых или сложных объектов.
Чуть ниже есть еще пояснение по вопросам возникшим у нашего коллеги:
сложные объекты (complex type) все имеют метод setData($property, $value). Но можно и по-другому, например setProperty($value). Перегоняется JSON, приходящий на API-сервис в объект, содержащий данные (аналог java beans).
Если что-то в описании непонятно — с удовольствием отвечу на вопросы.
flancer
22.08.2016 19:47-2Коллега Fesor, я не вижу ни вашего кода, исключающего излишнее дублирование в моем примере, ни ваших вопросов по постановке задачи.
Так дела не делаются.
Fesor
22.08.2016 22:06Коллега, flancer, когда у меня появится время я ВОЗМОЖНО потрачу его на вас. Пока у меня нет такой возможности.
Повторюсь — мокать тестируемый метод — это как минимум должно намекать что что-то пошло не так. А что именно — это уже ваши проблемы.
flancer
23.08.2016 06:13-3Спасибо за ответ, коллега. Когда мне нужен будет пример поверхностности и/или догматизма — я буду о вас вспоминать.
oxidmod
23.08.2016 06:45Ну например то, что ваш код не лучший пример рекурсии. Он нарушает SRP, что тянет за собой усложнение тестов
flancer
23.08.2016 10:04-1Допускаю, что лучший пример рекурсии для вас — вычисление факториала. И могу полностью с вами согласиться, что на "лучшем примере рекурсии" нет необходимости мокать собственно саму рекурсивную функцию, там нет никаких зависимостей и всего-то два прохода — первый и последний. Но в моем примере таких проходов 4 и плюс пара зависимостей, чье поведение нужно также замокать на соответствующую глубину. И если вы мне укажите, в каком месте моего примера нарушен благословенный SRP, то я буду премного вам благодарен и пересмотрю свои взгляды как минимум для данного примера. Пока же "нарушение SRP" в примере настолько же доказанный факт, как и "дублирование".
youlose
23.08.2016 10:13+1В вашей функции 2 вида входящих данных(массивы и объекты) + 2 вида результатов, тут и нарушается SRP (и поэтому у вас так всё осложнилось). Тут нужен какого-либо рода полиморфизм, чтобы тестировать её отдельно.
flancer
23.08.2016 14:38-1Входные данные — это ассоциативный массив и имя класса в который этот ассоциативный массив должен быть преобразован. Не вижу ничего страшного в том, что входные данные могут быть разных типов. А результат у меня один — объект. SRP не ограничивает кол-во и тип входных/выходных данных, насколько мне известно.
oxidmod
23.08.2016 10:32по поводу SRP я с вами уже дискутировал, но мы сошлись на том, что статья не о бест практис, а о рекурсии
flancer
23.08.2016 14:42принцип единственной обязанности (англ. Single responsibility principle) обозначает, что каждый объект должен иметь одну обязанность и эта обязанность должна быть полностью инкапсулирована в класс. Все его сервисы должны быть направлены исключительно на обеспечение этой обязанности.
Какую еще обязанность вы обнаружили в моем примере, кроме преобразования ассоциативного массива в объект заданного типа?
oxidmod
23.08.2016 14:51обязаность скорей преобразование массива в конкретный объект. Тогда зона отвественности этого преобразователя не расширяется с количеством объектов в системе.
у вас же классический год-обджект, который знает о всех объектах в системе. вы вынуждены вносить правки в этот преобразователь, чтобы добавить чтото нужное новому типу объектов (тем самым нарушая принцип открытости/закрытости)flancer
23.08.2016 15:11Как я говорил, преобразование только в объекты, аналогичные java-beans (вот определение). Там нет ничего, кроме акцессоров. Добавление новых типов объектов и новых свойств в существующие типы не влияет на существующий код. Никакого god object'а я не вижу — тупой функционал.
amarao
17.08.2016 16:48Я, вдруг, задумался, что в реальной жизни почти не вижу рекурсии per se. То есть может оказаться так, что для выполнения кода надо вызвать его же с другими аргументами, но это частный случай.
А вот сакрализация рекурсии — любимое развлечение computer science.lorc
17.08.2016 17:32+1Ну в реальной жизни она встречается при работе с рекурсивными типами данных (что в общем логично). Т.е. при работе с файловыми системами, сериализацией-десериализацией и обработкой других деревьев. Либо в тех языках, где нет циклов в обычном понимании этого слова (привет, erlang). Либо — всякие порождения computer science типа эффективных алгоритмов сортировки. Но эти алгоритмы уже везде реализованы и обычно лезть туда не надо.
Т.е. в реальной жизни рекурсия используется только там, где она реально нужна.
Другое дело, что 99% задач программировния в «реальной жизни» — это получить данные, провести над ними несложные преобразования, отдать данные дальше.
youlose
Столкнувшись на днях с необходимостью написать юнит-тест для рекурсивного метода я был неприятно удивлен необходимостью мокировать сам тестируемый метод.
Это может говорить о том что ваш рекурсивный метод возвращает результат разных типов? Это неверно.
Альтернатива — создавать такие входные данные, которые бы позволяли протестировать все ветки рекурсии в одном тестовом методе.
Зачем? Создайте нужное количество методов чтобы протестировать все разновидности входных и выходных данных. Сложная логика в тестах тоже неверно.