Может ли инвариантное тестирование заменить модульное тестирование? - PullRequest
32 голосов
/ 20 апреля 2009

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

В последнее время я играю с Haskell и его постоянной библиотекой тестирования QuickCheck. В отличие от TDD, QuickCheck делает акцент на тестировании инвариантов кода, то есть определенных свойств, которые сохраняются во всех (или основных подмножествах) входных данных. Быстрый пример: алгоритм стабильной сортировки должен давать тот же ответ, если мы запускаем его дважды, должен иметь увеличивающийся вывод, должен быть перестановкой ввода и т. Д. Затем QuickCheck генерирует различные случайные данные для проверки этих инвариантов.

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

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

Я сошел с ума, или я к чему-то?

Ответы [ 4 ]

22 голосов
/ 06 сентября 2010

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

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

9 голосов
/ 20 апреля 2009

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

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

1 голос
/ 19 ноября 2013

Сомнительный

Я слышал только о таких тестах (не использовал), но вижу две потенциальные проблемы. Я хотел бы получить комментарии о каждом.

Вводящие в заблуждение результаты

Я слышал о таких тестах, как:

  • reverse(reverse(list)) должно равняться list
  • unzip(zip(data)) должно равняться data

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

Мне кажется, что вы хотите проверить, например, что reverse([1 2 3]) равно [3 2 1], чтобы доказать правильное поведение хотя бы в одном случае, а затем добавить некоторое тестирование со случайными данными.

Сложность теста

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

Хороший юнит-тест, напротив, слишком прост, чтобы его можно было испортить или неправильно понять как читателя. Только опечатка может создать ошибку в «ожидайте, что reverse([1 2 3]) будет равно [3 2 1]».

0 голосов
/ 20 марта 2012

То, что вы написали в своем первоначальном посте, напомнило мне об этой проблеме, которая является открытым вопросом о том, что такое инвариант цикла для доказательства правильности цикла ...

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

...