Я несколько раз сталкивался с этой дилеммой. Мои модульные тесты должны копировать функциональность метода, который они тестируют для проверки его целостности? ИЛИ модульные тесты должны стремиться протестировать метод с многочисленными вручную созданными экземплярами исходных данных и ожидаемых выводов?
Я главным образом задаю вопрос для ситуаций, где метод, который Вы тестируете, довольно прост, и его правильное функционирование может быть проверено путем поглядывания на код в течение минуты.
Упрощенный пример (в рубине):
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
Я понимаю, что большинство раз метод, который Вы тестируете, не будет достаточно прост выровнять по ширине выполнение его этот путь. Но мой вопрос остается; действительно ли это - допустимая методология при некоторых обстоятельствах (почему или почему не)?
Это спорная позиция, но я считаю, что модульное тестирование с использованием производных значений намного превосходит использование произвольных жестко закодированных входных и выходных данных.
Проблема в том, что по мере того, как алгоритм становится даже немного сложным, связь между вводом и выводом становится неясной, если она представлена жестко закодированными значениями. Модульный тест оказывается постулатом . Технически это может работать, но ухудшает ремонтопригодность тестов , потому что приводит к Obscure Tests .
Использование производных значений для проверки результата устанавливает гораздо более четкую связь между входными данными теста и ожидаемыми выходными данными.
Аргумент о том, что это ничего не проверяет, просто неверен, потому что любой тестовый пример будет выполнять только часть пути через SUT, поэтому ни один тестовый пример не будет воспроизводить весь тестируемый алгоритм, а будет комбинация тесты сделают это.
Дополнительным преимуществом является то, что вы можете использовать меньше модульных тестов, чтобы охватить желаемую функциональность , и даже одновременно сделать их более коммуникативными. Конечный результат - более сжатые и удобные в обслуживании модульные тесты.
Unit-Test должен упражнять ваш код, а не что-то как часть языка, который вы используете.
Если логика кода заключается в конкатенации строк особым образом, вы должны тестировать именно это - в противном случае вам нужно полагаться на свой язык/фреймворк.
Наконец, вы должны создавать свои модульные тесты таким образом, чтобы они сначала отказывали "со смыслом". Другими словами, случайные значения не должны использоваться (если только вы не проверяете, что ваш генератор случайных чисел не возвращает один и тот же набор случайных значений!)
Тестирование функциональности с помощью одной и той же реализации ничего не проверяет. Если в одной из них есть ошибка, то и в другой она будет.
Но тестирование путем сравнения с альтернативной реализацией - это правильный подход. Например, вы можете протестировать итеративный (быстрый) метод вычисления чисел Фибоначчи, сравнив его с тривиальной рекурсивной, но медленной реализацией того же метода.
Разновидностью этого является использование реализации, которая работает только для особых случаев. Конечно, в этом случае вы можете использовать ее только для таких особых случаев.
При выборе входных значений использование случайных значений в большинстве случаев не очень эффективно. Я бы предпочел тщательно подобранные значения в любое время. В приведенном вами примере на ум приходят нулевые значения и очень длинные значения, которые при конкатенации не помещаются в строку.
Если вы используете случайные значения, убедитесь, что у вас есть способ воссоздать точный запуск с теми же случайными значениями, например, регистрируя значение seed, и имея возможность установить это значение во время запуска.
При модульном тестировании вам обязательно нужно вручную создавать тестовые примеры (ввод, вывод и ожидаемые побочные эффекты - это будут ожидания от вашего имитация объектов). Вы придумываете эти тестовые примеры таким образом, чтобы они охватывали всю функциональность вашего класса (например, охватываются все методы, все ветви всех операторов if и т. Д.). Подумайте об этом больше, как о создании документации вашего класса, показывая все возможные варианты использования.
Повторная реализация класса - не лучшая идея, потому что вы не только получите очевидное дублирование кода / функциональности, но также вполне вероятно, что вы внесете те же ошибки в эту новую реализацию.
Для проверки функциональности метода я бы использовал пары ввода и вывода везде, где это возможно. в противном случае вы можете скопировать и вставить функциональность, а также ошибки в ее реализации. что ты тогда тестируешь? вы бы проверяли, не изменилась ли функциональность (включая все ее ошибки) с течением времени. но вы бы не стали проверять правильность реализации.
проверка того, не изменилась ли функциональность с течением времени, может (временно) быть полезной во время рефакторинга. но как часто вы реорганизуете такие небольшие методы?
также модульные тесты можно рассматривать как документацию и как спецификацию входных и ожидаемых результатов метода. оба должны быть как можно более простыми, чтобы другие могли легко их прочитать и понять. как только вы вводите дополнительный код / логику в тест, его становится труднее читать.
ваш тест на самом деле выглядит как нечеткий тест . Нечеткие тесты могут быть очень полезны, но в модульных тестах следует избегать случайности из-за воспроизводимости.
Да. Меня это тоже беспокоит... хотя я бы сказал, что это более распространено при нетривиальных вычислениях. Чтобы избежать обновления теста при изменении кода, некоторые программисты пишут тест IsX=X, который всегда проходит успешно, независимо от SUT
Это не обязательно. В тесте можно указать ожидаемый результат, а не то, как вы его получили. Хотя в некоторых нетривиальных случаях это может сделать ваш тест более читабельным в плане того, как вы получили ожидаемое значение - тест как спецификация. Не стоит рефакторить это дублирование
def doubler(x); x * 2; end
def test_doubler()
input, expected = 10, doubler(10)
assert_equal expected, doubler(10)
end
Теперь, если я изменю doubler(x) на триплер, приведенный выше тест не провалится.
def doubler(x); x * 3; end
Однако этот тест будет:
def test_doubler()
assert_equal(20, doubler(10))
end
Вместо случайных наборов данных выберите статические репрезентативные точки данных для тестирования и используйте xUnit RowTest/TestCase для запуска теста с различными входными данными. Если n входных наборов идентичны для блока, выберите 1. Тест в OP может быть использован в качестве исследовательского теста или для определения дополнительных репрезентативных входных наборов. Юнит-тесты должны быть повторяемыми (см. q#61400) - Использование случайных значений противоречит этой цели.
Никогда не используйте случайные данные для ввода. Если ваш тест сообщает об ошибке, как вы когда-нибудь сможете его продублировать? И не используйте одну и ту же функцию для получения ожидаемого результата. Если у вас есть ошибка в вашем методе, вы, вероятно, поместите ту же ошибку в свой тест. Вычислите ожидаемые результаты другим методом.
Жестко запрограммированные значения прекрасно подходят, и убедитесь, что входные данные выбраны для представления всех нормальных и пограничных случаев. По крайней мере, проверьте ожидаемые входные данные, а также входные данные в неправильном формате или неправильном размере (например: нулевые значения).
Это действительно очень просто - модульный тест должен проверять, работает функция или нет. Это означает, что вам нужно предоставить ряд известных входных данных, которые имеют известные выходы, и протестировать их. Не существует универсального правильного способа сделать это. Однако использование одного и того же алгоритма для метода и проверки ничего не доказывает, кроме того, что вы умеете копировать / вставлять.