Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

Однако я подозреваю, что любой unit test можно заменить тестом, входы которого генерируются случайным образом. Даже в случае императивного кода "enter" - это порядок императивных утверждений, которые вам нужно сделать. Разумеется, вопрос о том, стоит ли создавать генератор случайных данных, и может ли вы сделать генератор случайных данных правильным выбором, - это еще один вопрос. Единичное тестирование - это просто вырожденный случай, когда случайный генератор всегда дает тот же результат.

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

Дезаделение результатов

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

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

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

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

Сложность тестирования

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

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