Тестирование против не повторяйся (СУХОЙ)

11

Почему повторение при написании тестов так сильно поощряется?

Кажется, что тесты в основном выражают то же самое, что и код, и, следовательно, являются дубликатом (в концепции, а не реализацией) кода. Не будет ли конечная цель DRY включать в себя устранение всего тестового кода?

Джон Ценг
источник

Ответы:

25

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

Тестовый код, который тестирует производственный код, совсем не похож. Я продемонстрирую в Python:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Тогда простой тест будет:

def test_multiply():
    assert multiply(4, 5) == 20

Обе функции имеют одинаковое определение, но обе делают разные вещи. Здесь нет повторяющегося кода. ;-)

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

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Представьте, что вы делаете это для 1000+ эффективных строк кода. Вместо этого вы тестируете для каждой функции:

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Теперь, когда функции добавляются / удаляются, мне нужно только рассмотреть возможность добавления / удаления одной тестовой функции.

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

siebz0r
источник
8
Технически рекомендуется одно утверждение на тест, поскольку это означает, что несколько проблем не будут отображаться как одна ошибка. Однако на практике, я думаю, что тщательная агрегация утверждений уменьшает количество повторяющегося кода, и я почти никогда не придерживаюсь одного утверждения на тестовое руководство.
Роб Черч
@ pink-diamond-square Я вижу, что NUnit не прекращает тестирование после того, как утверждение не выполнено (что я считаю странным). В этом конкретном случае действительно лучше иметь одно утверждение на тест. Если инфраструктура модульного тестирования останавливает тестирование после неудачного утверждения, лучше использовать несколько утверждений.
siebz0r
3
NUnit не останавливает весь набор тестов, но этот один тест останавливается, если вы не предпримете шаги, чтобы предотвратить его (вы можете поймать исключение, которое он выдает, что иногда полезно). Я думаю, что они утверждают, что если вы напишете тесты, включающие более одного утверждения, вы не получите всю информацию, необходимую для решения проблемы. Чтобы проработать ваш пример, представьте, что этой функции умножения не нравится число 3. В этом случае assert multiply(1,3)произойдет сбой, но вы также не получите отчет о неудачном тестировании assert multiply(3,4).
Роб Черч
Я просто подумал поднять его, потому что одно утверждение в тесте, из того, что я прочитал в мире .net, «хорошая практика» и множественные утверждения - это «прагматическое использование». Это выглядит немного иначе в документации Python, где пример def test_shuffleвыполняет два утверждения .
Rob Церковь
Я согласен и не согласен: D Здесь есть четкое повторение: assert multiply(*, *) == *чтобы вы могли определить assert_multiplyфункцию. В текущем сценарии это не имеет значения по количеству строк и удобочитаемости, но при более длительных тестах вы можете повторно использовать сложные утверждения, фикстуры, код, генерирующий фикстуры и т. Д. Я не знаю, является ли это наилучшей практикой, но я обычно делаю это.
inf3rno
10

Кажется, что тесты в основном выражают то же самое, что и код, и, следовательно, являются дубликатом

Нет, это не так

Тесты имеют другое назначение, чем ваша реализация:

  • Тесты удостоверяются, что ваша реализация работает.
  • Они служат документацией: просматривая тесты, вы видите контракты, которые должен выполнять ваш код, т.е. какие входные данные возвращают какой выходной, какие особые случаи и т. Д.
  • Кроме того, ваши тесты гарантируют, что при добавлении новых функций существующие функции не будут нарушены.
UOOO
источник
4

Нет. DRY - это написание кода только один раз для выполнения конкретной задачи, тестирование - проверка правильности выполнения задачи. Это несколько похоже на алгоритм голосования, где очевидно, что использование одного и того же кода было бы бесполезным.

jmoreno
источник
2

Не будет ли конечная цель DRY включать в себя устранение всего тестового кода?

Нет, конечная цель DRY на самом деле будет означать устранение всего производственного кода .

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

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

Я не думаю, что обратное (избавление от всех тестов) желательно, потому что:

  • Вы должны устранить несоответствие импеданса между реализацией и спецификацией. Производственный код может до определенной степени выражать намерение, но его никогда не будет так легко рассуждать, как хорошо выраженные тесты. Мы, люди, нуждаемся в более высоком представлении о том, почему мы строим вещи. Даже если вы не проводите тесты из-за СУХОГО, спецификации, возможно, придется в любом случае записывать в документах, что, безусловно, является более опасным явлением с точки зрения несоответствия импеданса и десинхронизации кода, если вы спросите меня.
  • Хотя рабочий код можно легко получить из правильных исполняемых спецификаций (при условии достаточного времени), набор тестов гораздо сложнее воссоздать из окончательного кода программы. Спецификации не выглядят четко, просто глядя на код, потому что взаимодействия между блоками кода во время выполнения трудно разобрать. Вот почему нам так тяжело работать с устаревшими приложениями без тестирования. Другими словами: если вы хотите, чтобы ваше приложение выживало более нескольких месяцев, лучше потерять жесткий диск, на котором находится производственная кодовая база, чем тот, на котором находится ваш набор тестов.
  • Гораздо проще случайно ввести ошибку в производственный код, чем в тестовый код. А поскольку производственный код не самопроверкаемый (хотя к нему можно обратиться с помощью Design by Contract или систем с более широкими возможностями), нам все еще нужна какая-то внешняя программа для его проверки и предупреждения о возникновении регрессии.
guillaume31
источник
1

Потому что иногда повторяться - это нормально. Ни один из этих принципов не предназначен для использования в любых обстоятельствах без вопросов или контекста. Время от времени я писал тесты против наивной (и медленной) версии алгоритма, что является довольно явным нарушением СУХОГО, но определенно выгодно.

U2EF1
источник
1

Поскольку модульное тестирование подразумевает усложнение непреднамеренных изменений , иногда оно может также усложнять и преднамеренные изменения . Этот факт действительно связан с принципом СУХОЙ.

Например, если у вас есть функция, MyFunctionкоторая вызывается в производственном коде только в одном месте, и вы пишете для нее 20 модульных тестов, вы можете легко получить в своем коде 21 место, где эта функция вызывается. Теперь, когда вам нужно изменить подпись MyFunction, или семантику, или и то, и другое (потому что некоторые требования меняются), у вас есть 21 место для изменения вместо одного. И причина, действительно, является нарушением принципа DRY: вы повторяли (по крайней мере) один и тот же вызов функции MyFunction21 раз.

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

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

Док Браун
источник
0

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

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

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

Тем не менее, вы, вероятно, делаете тестирование неправильно, если ваши тесты мало отличаются от реальных программ. Мсди предложил бы сосредоточиться больше на том, что в тестах, и меньше на том, как.

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

ChuckCottrill
источник
0

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

Жюль
источник
0

Модульные тесты не должны включать дублирование тестируемого кода, как уже отмечалось.

Я хотел бы добавить, однако, что модульные тесты, как правило, не такие СУХИЕ, как «производственный» код, потому что установка имеет тенденцию быть похожей (но не идентичной) во всех тестах ... особенно если у вас есть значительное количество зависимостей, над которыми вы издеваетесь / притворяется.
Конечно, возможно преобразовать подобные вещи в общий метод настройки (или набор методов настройки) ... но я обнаружил, что эти методы настройки имеют тенденцию иметь длинные списки параметров и быть довольно хрупкими.

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

Местный евангелист TDD / BDD объясняет это следующим образом:
«Ваш рабочий код должен быть СУХИМ. Но все в порядке, чтобы ваши тесты были« влажными »».

Дэвид
источник
0

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

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

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

inf3rno
источник