Является ли слежка за проверенным классом плохой практикой?

13

Я работаю над проектом, в котором внутренние вызовы класса обычны, но в результате получаются простые значения. Пример ( не реальный код ):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

Написание модульных тестов для этого типа кода действительно сложно, так как я просто хочу протестировать систему условий, а не реализацию реальных условий. (Я делаю это в отдельных тестах.) На самом деле было бы лучше, если бы я передавал функции, которые реализуют условия, а в тестах я просто предоставляю имитацию. Проблема с этим подходом - шумность: мы часто используем дженерики .

Рабочий раствор; однако сделать тестируемый объект шпионом и смоделировать вызовы внутренних функций.

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

Проблема здесь заключается в том, что реализация SUT эффективно изменяется, и может быть проблематично синхронизировать тесты с реализацией. Это правда? Есть ли лучшая практика, чтобы избежать этого хаоса внутренних вызовов методов?

Обратите внимание, что речь идет о частях алгоритма, поэтому разбиение его на несколько классов может оказаться нежелательным решением.

allprog
источник

Ответы:

15

Модульные тесты должны рассматривать классы, которые они тестируют, как черные ящики. Единственное, что имеет значение, это то, что его публичные методы ведут себя так, как ожидается. Каким образом класс достигает этого с помощью внутренних государственных и частных методов, не имеет значения.

Когда вы чувствуете, что невозможно создать значимые тесты таким образом, это признак того, что ваши классы слишком мощные и делают слишком много. Вы должны рассмотреть возможность перенести некоторые их функции в отдельные классы, которые можно тестировать отдельно.

Philipp
источник
1
Я давно понял идею модульного тестирования и успешно написал несколько из них. Это просто обманчиво, что на бумаге что-то выглядит просто, в коде это выглядит хуже, и, наконец, я столкнулся с чем-то, что имеет действительно простой интерфейс, но требует, чтобы я высмеивал половину мира вокруг входных данных.
allprog
@allprog Когда вам нужно много издеваться, кажется, что между вашими классами слишком много зависимостей. Вы пытались уменьшить связь между ними?
Филипп
@allprog, если вы находитесь в такой ситуации, виноват дизайн класса.
Брюс
Это модель данных, которая вызывает головную боль. Он должен придерживаться правил ORM и многих других требований. С помощью чистой бизнес-логики и кода без сохранения состояния гораздо проще получить правильные модульные тесты.
allprog
3
Модульные тесты не обязательно должны обрабатывать SUT как задний ящик. Вот почему они называются юнит-тестами. С помощью насмешек над зависимостями я могу влиять на окружающую среду, и чтобы знать, что мне нужно высмеивать, я должен знать и некоторые из них. Но, конечно, это не означает, что ТРИ следует менять каким-либо образом. Шпионаж, однако, позволяет внести некоторые изменения.
allprog
4

Если и то, findError()и другое checkFirstCondition()являются открытыми методами вашего класса, то findError()это фактически фасад для функциональности, которая уже доступна из того же API. В этом нет ничего плохого, но это означает, что вы должны написать для него тесты, которые очень похожи на уже существующие тесты. Это дублирование просто отражает дублирование в вашем общедоступном интерфейсе. Это не повод рассматривать этот метод иначе, чем другие.

Килиан Фот
источник
Внутренние методы обнародованы только потому, что они должны быть тестируемыми, и я не хочу создавать подклассы SUT или включать модульные тесты в класс SUT как статический внутренний класс. Но я понимаю вашу точку зрения. Тем не менее, я не мог найти хорошие ориентиры, чтобы избежать подобных ситуаций. Учебники всегда придерживаются базового уровня, который не имеет ничего общего с реальным программным обеспечением. В противном случае, причина для шпионажа состоит в том, чтобы избежать дублирования тестового кода и сделать область тестирования тестируемой.
allprog
3
Я не согласен с тем, что вспомогательные методы должны быть общедоступными для правильного модульного тестирования. Если в контракте метода указано, что он проверяет различные условия, то нет ничего плохого в том, чтобы написать несколько тестов для одного и того же открытого метода, по одному для каждого «субконтракта». Цель модульных тестов - добиться охвата всего кода, а не поверхностного охвата общедоступных методов с помощью соответствия метода 1: 1.
Килиан Фот
Использование только открытого API для тестирования во много раз значительно сложнее, чем тестирование внутренних частей по одному. Я не спорю, я понимаю, что этот подход не самый лучший, и у него есть ретроспективный взгляд, который показывает мой вопрос. Самая большая проблема заключается в том, что функции в Java не компонуются, а обходные пути чрезвычайно лаконичны. Но, похоже, нет другого решения для реального модульного тестирования.
allprog
4

Модульные тесты должны проверить контракт; это единственная важная вещь для них. Тестирование всего, что не является частью контракта, является не только пустой тратой времени, но и потенциальным источником ошибок. Каждый раз, когда вы видите, как разработчик меняет тесты, когда он меняет детали реализации, должны прозвучать сигналы тревоги; этот разработчик может (умышленно или нет) скрывать свои ошибки. Умышленное тестирование деталей реализации порождает эту дурную привычку, повышая вероятность того, что ошибки будут маскироваться.

Внутренние вызовы являются деталями реализации и должны представлять интерес только для измерения производительности . Который обычно не работа модульных тестов.

itsbruce
источник
Звучит здорово. Но на самом деле «строка», которую я должен набрать и назвать ее кодом, написана на языке, который очень мало знает о функциях. Теоретически я могу легко описать проблему и сделать замены здесь и там, чтобы упростить ее. В коде я должен добавить много синтаксического шума, чтобы добиться такой гибкости, которая отказывает мне в использовании. Если метод aсодержит вызов метода bв том же классе, то тесты aдолжны включать тесты b. И нет никакого способа изменить это, пока bэто не передается в aкачестве параметра. Но другого решения, я вижу, нет.
allprog
1
Если он bявляется частью общедоступного интерфейса, он все равно должен быть протестирован. Если это не так, его не нужно проверять. Если вы сделали это публично только потому, что хотели его протестировать, вы сделали неправильно.
Брюс
Смотрите мой комментарий к ответу @ Филиппа. Я еще не упомянул, но модель данных является источником зла. Чистый код без состояния - это кусок пирога.
allprog
2

Во-первых, мне интересно, что сложно проверить в примере функции, которую вы написали? Насколько я вижу, вы можете просто передать различные входные данные и проверить, чтобы убедиться, что возвращается правильное логическое значение. Что мне не хватает?

Что касается шпионов, то так называемое тестирование «белого ящика», в котором используются шпионы и имитаторы, - это на порядки больше работы для написания, не только потому, что написано гораздо больше тестового кода, но каждый раз, когда реализация изменено, вы также должны изменить тесты (даже если интерфейс остается прежним). И этот вид тестирования также менее надежен, чем тестирование черного ящика, потому что вам нужно убедиться, что весь этот дополнительный тестовый код корректен, и в то же время вы можете верить, что блочные тесты черного ящика не пройдут, если они не соответствуют интерфейсу. Вы не можете доверять этому из-за чрезмерного использования макетов, потому что иногда тест даже не тестирует много реального кода, а только макеты. Если макеты неверны, скорее всего, ваши тесты пройдут успешно, но ваш код все еще не работает.

Любой, кто имеет опыт в тестировании белого ящика, может сказать вам, что ему сложно писать и поддерживать. В сочетании с тем фактом, что они менее надежны, тестирование «белого ящика» в большинстве случаев значительно уступает.

BT
источник
Спасибо за примечание. Функция-пример на несколько порядков проще, чем что-либо, что вы должны написать в сложном алгоритме. На самом деле вопрос, похоже, больше похож на: проблематично ли тестировать алгоритмы со шпионами в нескольких частях. Это не код с состоянием, все состояния разделены на входные аргументы. Проблема заключается в том, что я хочу протестировать сложную функцию в примере, не предоставляя вменяемые параметры для подфункций.
allprog
С появлением функционального программирования в Java 8 это стало немного более элегантным, но все же сохранение функциональности в одном классе может быть лучшим выбором в случае алгоритмов, а не извлечение различных (одних бесполезных) частей в «использование один раз» классы только из-за тестируемости. В этом отношении шпионы делают то же самое, что и издевательства, но без визуального взрыва связного кода. На самом деле, тот же код настройки используется, как и с макетами. Мне нравится держаться подальше от крайностей, каждый тип теста может быть уместным в определенных местах. Тестирование как-то намного лучше, чем просто так. :)
allprog
«Я хочу проверить сложную функцию ... без необходимости предоставлять вменяемые параметры для подфункций» - я не понимаю, что вы имеете в виду. Какие подфункции? Вы говорите о внутренних функциях, используемых «сложной функцией»?
BT
Вот для чего в моем случае полезен шпионаж. Внутренние функции довольно сложны для управления. Не из-за кода, а потому, что они реализуют что-то логически сложное. Перемещение материала в другой класс является естественным вариантом, но эти функции сами по себе бесполезны. Поэтому лучше сохранить класс вместе и контролировать его с помощью шпионской функциональности. Работал безупречно в течение почти года, и мог легко противостоять изменениям модели. С тех пор я не использовал этот паттерн, просто подумал, что в некоторых случаях он жизнеспособен.
allprog
@allprog "логически сложный" - если он сложный, вам нужны сложные тесты. Обойти это невозможно. Шпионы только сделают это сложнее и сложнее для вас. Вы должны создавать понятные подфункции, которые вы можете тестировать самостоятельно, а не использовать шпионов для проверки их особого поведения внутри другой функции.
BT