Является ли дублированный код более приемлемым в модульных тестах?
Некоторое время назад я испортил несколько юнит-тестов, когда прошел и реорганизовал их, чтобы сделать их более СУХИМЫМИ- цель каждого теста больше не была ясна. Кажется, есть компромисс между удобочитаемостью тестов и ремонтопригодностью. Если я оставлю дублированный код в модульных тестах, они будут более удобочитаемыми, но если я изменю SUT, мне придется отслеживать и изменять каждую копию дублированного кода.
Согласны ли вы с тем, что этот компромисс существует? Если да, то предпочитаете ли вы, чтобы ваши тесты были читабельными или обслуживаемыми?
11 ответов
Дублированный код является запахом в коде модульного теста так же, как и в другом коде. Если у вас есть дублированный код в тестах, это затруднит рефакторинг кода реализации, потому что у вас непропорциональное количество тестов для обновления. Тесты должны помочь вам с уверенностью проводить рефакторинг, а не быть тяжелым бременем, мешающим вашей работе над тестируемым кодом.
Если дублирование уже установлено, рассмотрите возможность более широкого использования setUp
метод или предоставление более (или более гибких) методов создания.
Если дублирование происходит в коде, управляющем SUT, спросите себя, почему несколько так называемых "модульных" тестов выполняют одинаковую функциональность.
Если дублирование есть в утверждениях, то, возможно, вам понадобятся некоторые пользовательские утверждения. Например, если несколько тестов имеют строку утверждений, например:
assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())
Тогда, возможно, вам нужен один assertPersonEqual
метод, так что вы можете написать assertPersonEqual(Person('Joe', 'Bloggs', 23), person)
, (Или, возможно, вам просто нужно перегрузить оператор равенства на Person
.)
Как вы упоминаете, важно, чтобы тестовый код был читабельным. В частности, важно, чтобы цель теста была ясна. Я считаю, что если многие тесты выглядят в основном одинаково (например, три четверти строк одинаковы или практически одинаковы), то трудно обнаружить и распознать значительные различия без тщательного их прочтения и сравнения. Таким образом, я считаю, что рефакторинг для удаления дублирования помогает удобочитаемости, потому что каждая строка каждого метода теста имеет непосредственное отношение к цели теста. Это гораздо полезнее для читателя, чем случайная комбинация строк, которые имеют прямое отношение к делу, и строк, которые являются просто образцом.
Тем не менее, иногда тесты работают в сложных ситуациях, которые похожи, но все же значительно отличаются, и трудно найти хороший способ уменьшить дублирование. Придерживайтесь здравого смысла: если вы чувствуете, что тесты читабельны и проясняете их намерения, и, возможно, вам нужно обновлять больше, чем теоретически минимальное количество тестов при рефакторинге кода, вызванного тестами, то примите несовершенство и переместите на что-то более продуктивное. Вы всегда можете вернуться и провести рефакторинг тестов позже, когда придет вдохновение!
Читаемость важнее для тестов. Если тест не пройден, вы хотите, чтобы проблема была очевидной. Разработчику не нужно разбираться с большим количеством тщательно разработанного тестового кода, чтобы точно определить, что не удалось. Вы не хотите, чтобы ваш тестовый код стал настолько сложным, что вам нужно писать юнит-тест-тесты.
Тем не менее, устранение дублирования, как правило, хорошо, если оно ничего не скрывает, а устранение дублирования в ваших тестах может привести к улучшению API. Просто убедитесь, что вы не пройдете границу убывающей отдачи.
Код реализации и тесты - это разные животные, и к ним по-разному применяются правила факторинга.
Дублированный код или структура всегда являются запахом в коде реализации. Когда вы начинаете использовать шаблон в реализации, вам необходимо пересмотреть свои абстракции.
С другой стороны, тестовый код должен поддерживать уровень дублирования. Дублирование в тестовом коде позволяет достичь двух целей:
- Хранение тестов отделено. Чрезмерное связывание тестов может затруднить изменение одного неудачного теста, который требует обновления, поскольку контракт изменился.
- Хранение тестов значимым в изоляции. Когда один тест не проходит, должно быть достаточно просто выяснить, что именно он тестирует.
Я склонен игнорировать тривиальное дублирование в тестовом коде, пока каждый метод теста остается короче, чем около 20 строк. Мне нравится, когда ритм установки-запуска-проверки очевиден в методах тестирования.
Когда дублирование нарастает в части "проверки" тестов, часто бывает полезно определить пользовательские методы подтверждения. Конечно, эти методы должны по-прежнему проверять четко определенную связь, которая может быть очевидна в имени метода: assertPegFitsInHole
-> хорошо, assertPegIsGood
-> плохо.
Когда методы тестирования растут долго и повторяются, я иногда нахожу полезным определить шаблоны теста с заполнением пробелов, которые принимают несколько параметров. Затем фактические методы тестирования сводятся к вызову метода шаблона с соответствующими параметрами.
Что касается многих вещей в программировании и тестировании, то нет однозначного ответа. Вам нужно развить вкус, и лучший способ сделать это - совершать ошибки.
Вы можете уменьшить количество повторений, используя несколько различных методов тестирования.
Я более терпим к повторениям в тестовом коде, чем в рабочем коде, но иногда это меня расстраивает. Когда вы изменяете дизайн класса и вам нужно вернуться и настроить 10 различных методов тестирования, которые выполняют одинаковые шаги настройки, это разочаровывает.
Согласен. Компромисс существует, но отличается в разных местах.
Я более склонен к рефакторингу дублированного кода для настройки состояния. Но менее вероятно реорганизовать ту часть теста, которая фактически выполняет код. Тем не менее, если выполнение кода всегда занимает несколько строк кода, то я могу подумать, что это запах и рефакторинг реального тестируемого кода. И это улучшит читабельность и удобство сопровождения как кода, так и тестов.
Джей Филдс придумал фразу "DSL должны быть DAMP, а не DRY", где DAMP означает описательные и содержательные фразы. Я думаю, что то же самое относится и к тестам. Очевидно, что слишком много дублирования это плохо. Но удаление дублирования любой ценой еще хуже. Тесты должны действовать как спецификации, раскрывающие намерения. Если, например, вы указываете один и тот же объект под несколькими разными углами, то следует ожидать определенную степень дублирования.
"подверг их рефакторингу, чтобы сделать их более сухими - цель каждого теста больше не была ясна"
Похоже, у вас были проблемы с рефакторингом. Я просто догадываюсь, но если это окажется менее ясным, не значит ли это, что у вас еще есть над чем поработать, чтобы у вас были достаточно элегантные тесты, которые были бы совершенно ясными?
Вот почему тесты являются подклассом UnitTest - поэтому вы можете создавать хорошие тестовые наборы, которые будут правильными, легко проверяемыми и понятными.
В старину у нас были инструменты тестирования, которые использовали разные языки программирования. Было трудно (или невозможно) создавать приятные, удобные в работе тесты.
У вас есть все возможности - независимо от того, какой язык вы используете - Python, Java, C# - так что используйте этот язык хорошо. Вы можете создать красивый тестовый код, понятный и не слишком избыточный. Там нет компромисса.
Я чувствую, что тестовый код требует такого же уровня разработки, который обычно применяется к производственному коду. Конечно, могут быть аргументы в пользу читабельности, и я согласен, что это важно.
Однако по своему опыту я обнаружил, что хорошо продуманные тесты легче читать и понимать. Если есть 5 тестов, каждый из которых выглядит одинаково, за исключением одной измененной переменной и утверждения в конце, может быть очень трудно найти, что это за единичный элемент. Точно так же, если он учтен так, что видна только изменяющаяся переменная и утверждение, тогда легко понять, что тест делает немедленно.
Найти правильный уровень абстракции при тестировании может быть сложно, и я чувствую, что это стоит сделать.
Я люблю rspec из-за этого:
Есть 2 вещи, чтобы помочь -
общие примеры групп для тестирования общего поведения.
Вы можете определить набор тестов, а затем "включить" этот набор в ваши реальные тесты.вложенные контексты.
по сути, вы можете использовать метод 'setup' и 'teardown' для определенного подмножества ваших тестов, а не только для каждого в классе.
Чем раньше в.NET/Java/ других средах тестирования будут приняты эти методы, тем лучше (или вы могли бы использовать IronRuby или JRuby для написания ваших тестов, что, на мой взгляд, является лучшим вариантом)
В идеале, модульные тесты не должны сильно меняться после их написания, поэтому я склоняюсь к удобочитаемости.
Наличие как можно более дискретных юнит-тестов также помогает сфокусировать тесты на конкретной функциональности, на которую они ориентированы.
С учетом вышесказанного, я склонен пытаться повторно использовать определенные фрагменты кода, которые я использую многократно, например, код установки, который в точности совпадает с набором тестов.
Я не думаю, что есть связь между более дублированным и читаемым кодом. Я думаю, что ваш тестовый код должен быть таким же хорошим, как ваш другой код. Неповторяющийся код лучше читается, чем дублированный, если все сделано правильно.