Должен ли модуль-тест выполнять репликацию или тестовый выход?
Я столкнулся с этой дилеммой несколько раз. Должны ли мои модульные тесты дублировать функциональность метода, который они тестируют, чтобы проверить его целостность? ИЛИ Если модульные тесты стремятся протестировать метод с многочисленными созданными вручную экземплярами входов и ожидаемых выходов?
В основном я задаю вопрос о ситуациях, когда метод, который вы тестируете, достаточно прост, и его правильная работа может быть проверена, взглянув на код на минуту.
Упрощенный пример (в рубине):
def concat_strings(str1, str2)
return str1 + " AND " + str2
end
Упрощенная проверка функциональности для вышеуказанного метода:
def test_concat_strings
10.times do
str1 = random_string_generator
str2 = random_string_generator
assert_equal (str1 + " AND " + str2), concat_strings(str1, str2)
end
end
Я понимаю, что в большинстве случаев метод, который вы тестируете, не будет достаточно простым, чтобы оправдать это. Но мой вопрос остается; - это допустимая методология в некоторых случаях (почему или почему нет)?
Ответы
Ответ 1
Это спорная позиция, но я считаю, что модульное тестирование с использованием Derived Values намного превосходит использование произвольных жестко кодированный вход и выход.
Проблема заключается в том, что, поскольку алгоритм становится даже немного сложным, связь между входом и выходом становится неясной, если она представлена жестко закодированными значениями. unit test заканчивается постулатом. Он может работать технически, но болит в тестировании, потому что он приводит к Obscure Tests.
Использование Производные значения для проверки результата приводит к значительному более четкому отношению между тестовым вводом и ожидаемым выходом.
Аргумент о том, что это ничего не тестирует, просто неверен, потому что любой тестовый случай будет выполнять только часть пути через SUT, поэтому ни один тестовый пример не будет воспроизводить весь тестируемый алгоритм, а комбинация тесты сделают это.
Дополнительным преимуществом является то, что вы можете использовать меньше единичных тестов для покрытия желаемых функций и даже сделать их более коммуникабельными одновременно. Конечным результатом является упрощение и более ремонтные модульные тесты.
Ответ 2
Тестирование функциональности с использованием одной и той же реализации не проверяет ничего. Если в нем есть ошибка, другой будет также.
Но тестирование по сравнению с альтернативной реализацией является допустимым подходом. Например, вы можете протестировать итеративный (быстрый) метод вычисления чисел фибоначчи, сравнив его с тривиальной рекурсивной, но медленной реализацией того же метода.
Вариант этого использует реализацию, которая работает только для особых случаев. Конечно, в этом случае вы можете использовать его только для таких особых случаев.
При выборе входных значений использование случайных значений большую часть времени не очень эффективно. Я бы предпочел тщательно выбранные ценности в любое время. В примере, который вы указали, приходят на ум нулевые значения и чрезвычайно длинные значения, которые не будут вписываться в String при конкатенировании.
Если вы используете случайные значения, убедитесь, что у вас есть способ воссоздать точный запуск с одинаковыми случайными значениями, например, путем регистрации начального значения и наличия способа установить это значение во время запуска.
Ответ 3
В модульном тестировании вы обязательно должны вручную придумать тестовые примеры (так что вход, выход и какие побочные эффекты вы ожидаете - это будут ожидания от ваших макетных объектов). Вы придумываете эти тестовые примеры таким образом, чтобы они охватывали все функциональные возможности вашего класса (например, все методы охвачены, все ветки всех операторов if и т.д.). Подумайте об этом более подробно, создавая документацию своего класса, показывая все возможные способы использования.
Повторное выполнение класса не является хорошей идеей, потому что вы не только получаете очевидное дублирование кода/функциональности, но также вполне вероятно, что в этой новой реализации вы введете те же ошибки.
Ответ 4
чтобы проверить функциональность метода, я бы использовал входные и выходные пары, где это возможно. в противном случае вы можете скопировать и вставить функциональность, а также ошибки в ее реализации. что ты тогда испытываешь? вы будете тестировать, если функциональность (включая все ее ошибки) со временем не изменилась. но вы не будете тестировать правильность реализации.
тестирование, если функциональность не изменилась со временем, может (временно) быть полезным во время рефакторинга. но как часто вы реорганизуете такие небольшие методы?
также модульные тесты можно рассматривать как документацию и как спецификацию входов метода и ожидаемых результатов. оба должны быть максимально простыми, чтобы другие могли легко их прочитать и понять. как только вы вводите дополнительный код/логику в тест, становится труднее читать.
ваш тест на самом деле выглядит как тест fuzz. тесты fuzz могут быть очень полезными, но в модульных тестах следует избегать случайности из-за воспроизводимости.
Ответ 5
Единичный тест должен выполнять ваш код, а не что-то как часть языка, который вы используете.
Если логика кода заключается в конкатенации строк особым образом, вы должны проверить на это - в противном случае вам нужно полагаться на свой язык/фреймворк.
Наконец, вы должны создать свои модульные тесты для отказа сначала "со значением". Другими словами, случайные значения не должны использоваться (если вы не проверяете, что ваш генератор случайных чисел не возвращает один и тот же набор случайных значений!)
Ответ 6
Никогда не используйте случайные данные для ввода. Если ваш тест сообщает об ошибке, как вы сможете его дублировать? И не используйте одну и ту же функцию для генерации ожидаемого результата. Если у вас есть ошибка в вашем методе, вы, вероятно, поместите ту же ошибку в свой тест. Вычислить ожидаемые результаты другим методом.
Жестко-кодированные значения отлично подходят и обеспечивают выбор входов для отображения всех нормальных и крайних случаев. По крайней мере, проверьте ожидаемые входы, а также входы неправильного формата или неправильного размера (например, нулевые значения).
Это действительно довольно просто - a unit test должен проверить, работает ли функция или нет. Это означает, что вам нужно предоставить ряд известных входов, которые имеют известные выходы и протестируют их. Для этого нет универсального правильного способа. Однако использование того же алгоритма для метода и проверки не доказывает ничего, кроме того, что вы умеете копировать/вставлять.
Ответ 7
Да. Меня это тоже беспокоит... хотя я бы сказал, что он более распространен с нетривиальными вычислениями. Чтобы избежать обновления теста при изменении кода, некоторые программисты записывают тест IsX = X, который всегда выполняется независимо от SUT
- О возможности дублирования
Вам не обязательно. В вашем тесте можно указать, каков ожидаемый результат, а не как вы его получили.
Хотя в некоторых нетривиальных случаях это может сделать ваш тест более понятным относительно того, как вы вывели ожидаемое значение - тест как спецификацию. Вы не должны реорганизовывать это дублирование.
def doubler(x); x * 2; end
def test_doubler()
input, expected = 10, doubler(10)
assert_equal expected, doubler(10)
end
Теперь, если я изменю удвоение (x) на триплер, вышеуказанный тест не подведет.
def doubler(x); x * 3; end
Однако это одно:
def test_doubler()
assert_equal(20, doubler(10))
end
- случайность в модульных тестах - не.
Вместо случайных наборов данных выберите статические репрезентативные точки данных для тестирования и используйте xUnit RowTest/TestCase для запуска теста с вводом данных diff. Если n единиц ввода идентичны для устройства, выберите 1.
Тест в ОП можно было использовать в качестве исследовательского теста/или для определения дополнительных репрезентативных наборов ввода. Единичные тесты должны быть повторяемыми (см. Q # 61400). Использование случайных значений поражает эту цель.