Является ли дублированный код более терпимым в модульных тестах?

113

Некоторое время назад я испортил несколько модульных тестов, когда прошел и реорганизовал их, чтобы сделать их более СУХИМИ - цель каждого теста больше не была ясна. Похоже, существует компромисс между удобочитаемостью тестов и ремонтопригодностью. Если я оставлю дублированный код в модульных тестах, они станут более читаемыми, но если я изменю SUT , мне придется отслеживать и изменять каждую копию дублированного кода.

Вы согласны с тем, что этот компромисс существует? Если да, то предпочитаете ли вы, чтобы ваши тесты были удобочитаемыми или поддерживаемыми?

Дэрил Спитцер
источник

Ответы:

68

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

Если дублирование уже настроено, рассмотрите возможность более широкого использования setUpметода или предоставления более (или более гибких) методов создания .

Если дублирование заключается в коде, управляющем SUT, спросите себя, почему несколько так называемых «модульных» тестов реализуют одни и те же функции.

Если дублирование есть в утверждениях, возможно, вам понадобятся какие-то настраиваемые утверждения . Например, если в нескольких тестах есть строка утверждений вроде:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

Тогда, возможно, вам понадобится единственный assertPersonEqualметод, чтобы вы могли писать assertPersonEqual(Person('Joe', 'Bloggs', 23), person). (Или, возможно, вам просто нужно перегрузить оператор равенства Person.)

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

Тем не менее, иногда тесты используют сложные ситуации, которые похожи, но все же значительно отличаются, и трудно найти хороший способ уменьшить дублирование. Используйте здравый смысл: если вы чувствуете, что тесты удобочитаемы и проясняют их намерения, и вам удобно, что, возможно, вам нужно обновить больше, чем теоретически минимальное количество тестов при рефакторинге кода, вызванного тестами, тогда примите несовершенство и переместите на что-нибудь более продуктивное. Вы всегда можете вернуться и провести рефакторинг тестов позже, когда придет вдохновение!

спекулянт
источник
30
«Дублированный код - это такой же запах в коде модульного теста, как и в другом коде». Нет. «Если у вас есть дублированный код в тестах, это затрудняет рефакторинг кода реализации, потому что вам нужно обновить непропорционально большое количество тестов». Это происходит потому, что вы тестируете частный API вместо общедоступного.
15
Но для предотвращения дублирования кода в модульных тестах обычно требуется вводить новую логику. Я не думаю, что модульные тесты должны содержать логику, потому что тогда вам понадобятся модульные тесты модульных тестов.
Петр Пеллер
@ user11617 определите "частный API" и "общедоступный API". Насколько я понимаю, общедоступный Api - это Api, видимый для внешнего мира / сторонних потребителей и явно версируемый через SemVer или аналогичный, все остальное является частным. С этим определением почти все модульные тесты тестируют «частный API» и, следовательно, более чувствительны к дублированию кода, что, я думаю, верно.
KolA 09
@KolA "Public" не означает сторонних потребителей - это не веб-API. Открытый API класса относится к методам, которые предназначены для использования клиентским кодом (который обычно не / не должен сильно меняться) - обычно это «общедоступные» методы. Частный API относится к логике и методам, которые используются внутри компании. К ним нельзя обращаться извне класса. Это одна из причин, по которой важно правильно инкапсулировать логику в классе, используя модификаторы доступа или соглашения на используемом языке.
Натан
@Nathan у любого пакета библиотеки / dll / nuget есть сторонние потребители, это не обязательно должен быть веб-API. Я упомянул, что очень часто объявляют общедоступные классы и члены, которые не должны использоваться напрямую потребителями библиотеки (или, в лучшем случае, делать их внутренними и аннотировать сборку с помощью InternalsVisibleToAttribute), просто чтобы позволить модульным тестам напрямую обращаться к ним. Это приводит к огромному количеству тестов в сочетании с реализацией и делает их больше обузой, чем преимуществом,
KolA
186

Для тестов важнее читаемость. Если тест не прошел, вы хотите, чтобы проблема была очевидна. Разработчику не нужно продираться через большое количество тщательно продуманного тестового кода, чтобы точно определить, что именно не удалось. Вы же не хотите, чтобы ваш тестовый код стал настолько сложным, что вам нужно было писать модульные тесты.

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

Кристофер Джонсон
источник
xUnit и другие содержат аргумент «сообщение» в вызовах assert. Хорошая идея использовать содержательные фразы, чтобы разработчики могли быстро находить неудавшиеся результаты тестов.
seand
1
@seand. Вы можете попытаться объяснить, что проверяет ваше утверждение, но если он не работает и содержит несколько неясный код, разработчику все равно придется пойти и развернуть его. ИМО. Более важно, чтобы там был код, описывающий себя.
IgorK 06
1
@ Кристофер,? Почему это размещено в вики сообщества?
Pacerier 03
@Pacerier Я не знаю. Раньше существовали сложные правила о том, что вещи автоматически становятся вики сообщества.
Кристофер Джонсон
Для удобочитаемости отчетов важнее, чем тесты, особенно при выполнении интеграции или сквозного тестирования, сценарии могут быть достаточно сложными, чтобы избежать крошечной навигации, это нормально, чтобы найти сбой, но снова для меня сбой в отчетах должен достаточно хорошо объяснить проблему.
Anirudh 06
47

Код реализации и тесты - это разные животные, и правила факторинга к ним применяются по-разному.

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

С другой стороны, код тестирования должен поддерживать уровень дублирования. Дублирование тестового кода позволяет достичь двух целей:

  • Разделение тестов. Чрезмерное связывание тестов может затруднить изменение одного неудачного теста, который требует обновления из-за изменения контракта.
  • Сохранение смысла тестов изолированно. Когда один тест не работает, должно быть достаточно просто выяснить, что именно он тестирует.

Я стараюсь игнорировать тривиальное дублирование в тестовом коде, если каждый тестовый метод остается короче примерно 20 строк. Мне нравится, когда в методах тестирования проявляется ритм «настройка-запуск-проверка».

Когда дублирование закрадывается в тестовую часть «проверки», часто бывает полезно определить собственные методы утверждения. Конечно, эти методы должны по-прежнему проверять четко идентифицированную связь, которая может быть очевидна в имени метода: assertPegFitsInHole-> хорошо, assertPegIsGood-> плохо.

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

По многим вещам в программировании и тестировании однозначного ответа нет. Вам нужно развить вкус, и лучший способ сделать это - ошибаться.

ddaa
источник
8

Я согласен. Компромисс существует, но он отличается в разных местах.

Я с большей вероятностью реорганизую дублированный код для настройки состояния. Но менее вероятно, что придется провести рефакторинг той части теста, которая фактически проверяет код. Тем не менее, если для выполнения кода всегда требуется несколько строк кода, я могу подумать, что это запах и рефакторинг реального тестируемого кода. И это улучшит читаемость и ремонтопригодность как кода, так и тестов.

штукатурка
источник
Думаю, это хорошая идея. Если у вас много дублирования, посмотрите, можете ли вы провести рефакторинг для создания общего «тестового устройства», в котором могут выполняться многие тесты. Это устранит повторяющийся код установки / удаления.
Outlaw Programmer
8

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

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

Дон Киркби
источник
6

Джей Филдс придумал фразу, что «DSL должны быть DAMP, а не DRY», где DAMP означает описательные и содержательные фразы . Думаю, то же самое можно сказать и о тестах. Очевидно, что слишком много дублирования - это плохо. Но удалить дублирование любой ценой еще хуже. Тесты должны действовать как спецификации, раскрывающие намерения. Если, например, вы указываете один и тот же объект с разных точек зрения, следует ожидать некоторого дублирования.

Йорг В. Миттаг
источник
3

Я ЛЮБЛЮ rspec из-за этого:

У него есть 2 вещи, которые могут помочь -

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

  • вложенные контексты.
    по сути, вы можете иметь методы «настройки» и «разборки» для определенного подмножества тестов, а не только для каждого в классе.

Чем раньше .NET / Java / другие тестовые среды примут эти методы, тем лучше (или вы могли бы использовать IronRuby или JRuby для написания своих тестов, что, по моему мнению, является лучшим вариантом)

Орион Эдвардс
источник
3

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

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

Поиск подходящего уровня абстракции при тестировании может быть трудным, и я считаю, что это того стоит.

Кевин Лондон
источник
2

Я не думаю, что существует связь между более дублированным и читаемым кодом. Я думаю, что ваш тестовый код должен быть не хуже других. Неповторяющийся код более читабелен, чем дублированный код, если он выполнен правильно.

Paco
источник
2

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

Максимально дискретные модульные тесты помогают сосредоточить тесты на конкретной функциональности, на которую они нацелены.

С учетом сказанного, я стараюсь повторно использовать определенные фрагменты кода, которые я использую снова и снова, например, установочный код, который абсолютно одинаков во всех наборах тестов.

17 из 26
источник
2

"переработал их, чтобы сделать их более СУХИМИ - цель каждого теста больше не была ясна"

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

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

Раньше у нас были инструменты для тестирования, которые использовали разные языки программирования. Было сложно (или невозможно) создать приятные, простые в работе тесты.

У вас есть все возможности - независимо от того, какой язык вы используете - Python, Java, C # - так что используйте этот язык как следует. Вы можете получить красивый тестовый код, понятный и не слишком избыточный. Нет компромисса.

С. Лотт
источник