Может ли инвариантное тестирование заменить модульное тестирование?

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

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

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

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

Я сошел с ума или я что-то понял?


person Edward Z. Yang    schedule 20.04.2009    source источник
comment
Я делал это недавно (в erlang), чтобы понять, как смоделировать вашу систему, требуется немного больше работы, но вы можете это сделать. И когда вы это сделаете, вы знаете, что ваш код хорош   -  person Zachary K    schedule 12.05.2014


Ответы (4)


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

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

person Edward Z. Yang    schedule 06.09.2010
comment
Вы правы, он не может заменить более общие модульные тесты, НО, он определенно может дополнить общие усилия по тестированию. - person tomosius; 11.03.2017
comment
Разве вы не можете просто использовать одно и то же случайное семя? Или как-то сохранить сгенерированный случайный ввод? Этот ответ, по-видимому, основан на потенциальном отсутствии функциональности в фреймворках тестирования на основе свойств, а не на возможностях самого тестирования на основе свойств. - person lastmjs; 20.03.2017

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

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

person Anthony    schedule 20.04.2009
comment
Неплохо подмечено! И следующий вопрос: можем ли мы подогнать его под императивный код? - person Edward Z. Yang; 20.04.2009
comment
Определенно может - но это тот же аргумент, что и выбор языковой парадигмы в первую очередь; они все равны, но есть места, где некоторые вещи просто не подходят. Думаю, это один из таких случаев. - person Anthony; 24.04.2009

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

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

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

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

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

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

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

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

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

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

person Nathan Long    schedule 19.11.2013
comment
Лично я не пишу reverse = id, поэтому я достаточно уверен, что если я действительно попытался реализовать его правильно, и он прошел это reverse . reverse = id, то это, вероятно, правильно - person alternative; 22.11.2014
comment
Настоящий инвариант, который вам нужен, - это forall x xs i, (x:xs) !! j == reverse (x:xs) !! (n - j), где n = length xs, j = mod i (n + 1). - person pyon; 17.01.2016
comment
Я могу гарантировать, что все, что удовлетворяет указанному выше инварианту, а также тривиальному reverse [] = [], является добросовестной функцией реверса. Модульное тестирование не требуется. - person pyon; 18.01.2016

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

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

person Community    schedule 20.03.2012