Это плохая практика, чтобы основывать ожидаемые результаты от реальных результатов в модульном тестировании? - PullRequest
2 голосов
/ 09 апреля 2019

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

Допустим, есть простая функция, которая возвращает строки с некоторыми аргументами.

generate_string(name, date) #  Function to test
    result 'My Name is {name} I was born on {date} and this isn't my first rodeo'

----Test----

setUp
    name = 'John Doe'
    date = '1990-01-01'

test_that_generate_string_function
    ...
    expected = 'My Name is John Doe I was born on 1990-01-01 and this isn't my first rodeo'
    assertEquals(expected, actual)

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

test_date_hardcoded_method
    ...
    date = 1990-01-01
    actual = generate_string(name, date)
    expected = 'My Name is John Doe I was born on 1990-01-01 and this isn't my first rodeo'

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

test_date_deconstucted_method
    ...
    date = get_date()
    actual = generate_string(name, date)
    actual_deconstructed = actual.split(' ')
    actual_deconstructed[-7] = '1990-01-01'  # Hard code small expected change
    expected = join.actual_deconstructed
    assertEquals(expected, actual)

Я закончил тем, что создал два тестовых блока, используя каждый метод, чтобы увидеть, могу ли я понять, откуда они берутся, но я просто не вижу этого. Когда все ожидаемые результаты жестко запрограммированы, любое небольшое изменение приводит к провалу подавляющего большинства тестов. Если «нет» должно быть «нет», то hardcoed_method потерпит неудачу, пока кто-то не изменит вещи вручную. Вист, метод deconstructed_method заботится только о дате и все равно пройдет ее тест. Он потерпит неудачу, только если с датой произойдет что-то неожиданное. Только после нескольких неудачных тестов после внесения изменений кто-то другой сделал очень просто точно определить, что пошло не так, как я и думал, в этом и заключался весь смысл модульного тестирования.

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

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

Ответы [ 3 ]

2 голосов
/ 10 апреля 2019

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

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

0 голосов
/ 18 апреля 2019

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

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

Тогда яЯ программировал с кем-то, у кого на два десятилетия больше опыта, чем у меня, который, как я знаю, программист мирового уровняОн сказал: «Ваши тесты слишком повторяющиеся, сделайте их рефакторинг, чтобы сделать их менее хрупкими».Я сказал: «Я думал, что мои тесты должны быть очень простыми и очевидными, а это значит, что мой код должен быть повторяющимся».И он сказал: «Не пишите свой тестовый код так, чтобы он отличался от вашего производственного кода, сохраняйте их СУХИМ (не повторяйте себя)».

Затем возник целый класс мета-вопросов о моей программе,Что достаточно для тестирования кода?Что такое хороший тестовый код?

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

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

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

Очевидно, вы не можетеоцените язык, который вы используете в вашей компании.Итак, вот один совет, который мой друг сказал, который придерживается меня: тестовый код должен быть похож на рабочий код, поэтому не повторяйте себя.Если вы обнаружите, что ваши тесты становятся повторяющимися, удалите их.Сохраните минимальное количество тестов, которые сломаются, если логика нарушена.Не держите пятьдесят нечетных тестов, которые охватывают все варианты конкатенации строк.Это «чрезмерное тестирование». Избыточное тестирование препятствует рефакторингу, добавляя функциональность и устраняя технические долги, а не устраняет ошибки.В некоторых языках это означает написание множества повторяющихся тестов, которые вам необходимы для проверки вашей логики, когда вы пишете ее как скаффолдинг.Затем, когда у вас все получится, напишите более масштабные тесты, которые будут ломаться, если кто-то разбивает подпункты, и удаляйте все повторяющиеся тесты, чтобы не оставить «тестовый долг».Это приводит к нескольким грубым тестам, которые являются чрезвычайно простыми без большого количества повторений.

0 голосов
/ 17 апреля 2019

У вас есть функция, которая генерирует строку из входных данных. Существует возможность иметь тестовые случаи, которые всегда проверяют всю сгенерированную строку, хотя цель каждого теста - проверить очень специфическую часть этой строки. Вы правильно считаете этот подход плохим: итоговые тесты будут слишком широкими и, следовательно, хрупкими. Они потерпят неудачу / должны быть сохранены для любого изменения, не только в случае изменений, которые затрагивают определенную часть сгенерированной строки. Возможно, вам будет полезно взглянуть на обсуждение Месаросом хрупких тестов, в частности, части, где «Тест говорит слишком много о том, как программное обеспечение должно быть структурировано или вести себя»: http://xunitpatterns.com/Fragile%20Test.html#Overspecified%20Software

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

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

test_date_part_of_generated_string:
   date = 1990-01-01
   actual_full_string = generate_string(name, date)
   actual_string_parts = actual_full_string.split(' ')
   actual_date_part = actual_string_parts[-7]
   assertEquals('1990-01-01', actual_date_part)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...