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

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

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


person Daryl Spitzer    schedule 24.09.2008    source источник


Ответы (11)


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

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

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

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

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

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

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

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

person spiv    schedule 27.09.2008
comment
Дублированный код - это такой же запах в коде модульного теста, как и в другом коде. Нет. Если вы дублируете код в тестах, это затрудняет рефакторинг кода реализации, потому что вам нужно обновить непропорционально большое количество тестов. Это происходит потому, что вы тестируете частный API вместо общедоступного. - person ; 18.01.2013
comment
Но для предотвращения дублирования кода в модульных тестах обычно требуется вводить новую логику. Я не думаю, что модульные тесты должны содержать логику, потому что тогда вам понадобятся модульные тесты модульных тестов. - person Petr Peller; 27.12.2015
comment
@ user11617, пожалуйста, определите частный API и общедоступный API. Насколько я понимаю, общедоступный Api - это Api, видимый для внешнего мира / сторонних потребителей и явно версируемый через SemVer или аналогичный, все остальное является частным. С этим определением почти все модульные тесты тестируют частный API и, следовательно, более чувствительны к дублированию кода, что, я думаю, верно. - person KolA; 10.04.2019
comment
@KolA Public не означает сторонних потребителей - это не веб-API. Открытый API класса относится к методам, которые предназначены для использования клиентским кодом (который обычно не / не должен сильно меняться) - обычно это общедоступные методы. Частный API относится к логике и методам, которые используются внутри компании. К ним нельзя обращаться извне класса. Это одна из причин, по которой важно правильно инкапсулировать логику в классе, используя модификаторы доступа или соглашения на используемом языке. - person Nathan; 25.07.2019
comment
@Nathan у любого пакета библиотеки / dll / nuget есть сторонние потребители, это не обязательно должен быть веб-API. Я упомянул, что очень часто объявляют общедоступные классы и члены, которые не должны использоваться напрямую потребителями библиотеки (или, в лучшем случае, делать их внутренними и аннотировать сборку с помощью InternalsVisibleToAttribute), просто чтобы позволить модульным тестам напрямую обращаться к ним. Это приводит к огромному количеству тестов в сочетании с реализацией и делает их больше обузой, чем преимуществом. - person KolA; 26.07.2019
comment
Как ни странно, я согласен с ответами Спива и Кристофера Джонсона. Я бы объяснил это так: всегда нужно избегать дублирования деталей реализации в тестах, но никогда за счет сокрытия контекста. Плохой выбор для удаления дублирования - это цепочки наследования и композиция типов, которые скрывают контекст. Хороший выбор для удаления дублирования - Фабрики, Строители и Матери объектов. - person Derek Greer; 09.03.2021

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

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

person Kristopher Johnson    schedule 24.09.2008
comment
xUnit и другие содержат аргумент «сообщение» в вызовах assert. Хорошая идея - использовать содержательные фразы, чтобы разработчики могли быстро находить неудавшиеся результаты тестов. - person seand; 31.03.2012
comment
@seand. Вы можете попытаться объяснить, что проверяет ваше утверждение, но если он не работает и содержит несколько неясный код, разработчику все равно придется пойти и раскрутить его. ИМО. Более важно, чтобы там был код, описывающий себя. - person IgorK; 07.10.2012
comment
Для удобочитаемости отчетов важнее, чем тесты, особенно при выполнении интеграции или сквозного тестирования, сценарии могут быть достаточно сложными, чтобы избежать крошечной навигации, это нормально, чтобы найти сбой, но опять же для меня сбой в отчетах должен достаточно хорошо объяснить проблему. - person Anirudh; 06.06.2018
comment
Читаемость одинаково важна как в тестовом, так и в нетестовом коде. Рефакторинг не должен ухудшать читабельность - он должен улучшить читаемость. - person Logan Pickup; 11.12.2019
comment
Хорошее обоснование того, что здесь предлагается, исходит из фундаментальных предположений, лежащих в основе DRY и модульного тестирования: повторяться - плохо, потому что это умножает работу, необходимую для изменения поведения. С другой стороны, если вам нужно часто менять модульные тесты, велика вероятность, что ваши модульные тесты слишком строгие - они проверяют как вместо what. Идеальный модульный тест не меняется после того, как он был выложен. Это означает, что применение DRY к модульным тестам не приносит такой большой пользы, как при применении к коду приложения. DRY - это реакция на изменения. Модульные тесты не должны. - person Błażej Michalik; 06.12.2020

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

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

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

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

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

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

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

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

person ddaa    schedule 24.09.2008
comment
+1 за то, что мне нравится, когда в методах тестирования проявляется ритм «настройка-запуск-проверка». именно то, что я собирался сказать. Поэтому я бы дважды подумал, прежде чем извлекать общий метод, который выполняет две или более фазы настройки-запуска-проверки. - person spikemanuk; 09.03.2021

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

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

person stucampbell    schedule 24.09.2008
comment
Думаю, это хорошая идея. Если у вас много дублирования, посмотрите, сможете ли вы провести рефакторинг для создания общего тестового устройства, в котором могут выполняться многие тесты. Это устранит повторяющийся код установки / удаления. - person Outlaw Programmer; 25.09.2008

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

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

person Don Kirkby    schedule 24.09.2008

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

person Jörg W Mittag    schedule 28.09.2008

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

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

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

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

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

person Orion Edwards    schedule 25.09.2008

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

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

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

person Kevin London    schedule 02.05.2015

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

person Paco    schedule 24.09.2008

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

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

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

person 17 of 26    schedule 24.09.2008

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

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

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

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

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

person S.Lott    schedule 25.09.2008