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

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

Однако в этой статье мы рассмотрим другой способ написания тестов, так что один тест охватывает множество различных входных данных, а не один вход. Для этого мы сильно полагаемся на рандомизацию для генерации нужных нам входных данных. Для примеров кода мы будем использовать язык F # для краткости и использовать библиотеку FsCheck и Xunit для тестирования. Это не должно мешать вам читать дальше, так как FsCheck также можно использовать с C #.

Единичные тесты

Классический модульный тест состоит из трех A: Arrange, Act, Assert. Мы расставляем компоненты и подготавливаем входы. Мы действуем, вызывая логику, которую хотим протестировать с входом, и, наконец, мы проверяем, что наши утверждения о выходе верны. Для этого мы обычно подготавливаем различные входные данные с соответствующими выходными данными и вызываем логику для проверки. Это называется набором тестов. Иногда, если настройки идентичны, мы можем параметризовать тест, добавив несколько комбинаций входных данных с соответствующими выходными данными к одному и тому же модульному тесту.

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

Итак, мы написали три тестовых примера: один для положительного числа, один для отрицательного и один для нулевого. Покрытие кода выглядит великолепно, у нас есть тесты как на отрицательные, так и на положительные входные данные, что еще нужно сделать? Проницательный читатель или опытный разработчик может подумать, а как же граничные случаи? В данном случае число -2 147 483 648. Как наша реализация справится с этим и является ли такое поведение намеренным?

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

Тестирование на основе собственности

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

  • Результат abs x всегда должен быть больше или равен 0 независимо от значения x. Математически записанный abs x ≥ 0
  • Для любого отрицательного числа x при добавлении к нему abs x результат должен быть 0. Математически записывается как: Для x ‹0: x + abs x = 0

Написание рандомизированных тестов на основе свойств, охватывающих эти свойства, легко выполняется с помощью FsCheck и его интеграции с Xunit в приведенном ниже коде.

Первое, на что следует обратить внимание, это то, что вместо того, чтобы писать атрибуты Fact, мы теперь используем атрибуты Property. Второе, на что следует обратить внимание, это то, что наши функции больше не являются без аргументов и больше не имеют единицы в качестве возвращаемого типа. Вместо этого они принимают входные данные, которые мы ожидаем передать тестируемой функции, а возвращаемый тип является логическим, представляющим, удерживается ли свойство, которое мы хотим проверить, для конкретного входа или нет. Когда FsCheck видит такой код, он заглядывает в свою библиотеку генераторов и начинает выдавать случайные входные данные этого типа. Для каждого ввода он проверяет, выполняется ли свойство. FsCheck использует концепцию произвольного значения для генерации случайных значений. Произвольные состоят из генератора, отвечающего за генерацию случайного ввода, и средства сжатия, отвечающего за сжатие случайного ввода до простейшего возможного после того, как обнаруживается ввод, нарушающий свойство в тесте. FsCheck имеет несколько встроенных вспомогательных типов. IntWithMinMax обеспечивает включение int.MinValue и int.MaxValue, а NegativeInt возвращает только отрицательные целые числа. Тип DoNotShrink гарантирует, что усадочная машина не работает.

После выполнения проверок очень высока вероятность того, что вы увидите следующую ошибку или что-то похожее на нее.

Test Name: abs x >= 0
Test Outcome: Failed
Result Message: 
FsCheck.Xunit.PropertyFailedException : 
Falsifiable, after 67 tests (0 shrinks) (StdGen (167901972, 296833629)):
Original:
DoNotShrink (IntWithMinMax -2147483648)

Эта ошибка сообщает вам, что после генерации 67 случайных входов (FsCheck по умолчанию будет 100 случайных входов), он наткнулся на один вход со значением: -2 147 483 648, где абсолютное значение не является положительным. Это, конечно, из-за старой проблемы с дополнением двух.

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

Вход блокировки: рыбалка в мутной воде

Когда вы имеете дело со случайным тестированием и у вас не проходит тест на основе свойств, нет гарантии, что вы снова увидите этот неверный ввод при следующем запуске теста. К счастью, FsCheck сообщит вам, какое семя было использовано для создания неверного ввода. Это то, что означает строка StdGen (167901972, 296833629) на выходе. Вы можете использовать эту информацию, чтобы воспроизвести проблему. Например, вы можете настроить классический тест Xunit, в котором вы заставляете FsCheck использовать исходные начальные числа для рандомизатора. Это позволяет вам воспроизводить неверный ввод снова и снова, пока вы не найдете и не исправите ошибку.

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

Разработка тестов на основе свойств

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

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

  • Отмена заказа, когда книга заказов пуста, приводит к пустой книге заказов.
  • Размещение и отмена заказа с тем же идентификатором приводит к пустой книге заказов.
  • Размещение ордера в книге, а затем равный и противоположный ордер приводит к пустой книге заказов.
  • При добавлении ордера в пустую книгу заказов он помещается в книгу заказов как лучший бид или аск.

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

Каждая проверка на основе свойств принимает аргумент типа Order.t, и FsCheck знает, как его сгенерировать, поскольку это тип записи F #. Если вы хотите сгенерировать общие объекты, такие как экземпляры классов с изменяемыми полями, вам потребуется немного больше работы. Мы вернемся к этому чуть позже в статье. В каждом из наших тестов мы также пытались указать свойства в терминах операций с объектами домена для повышения удобочитаемости.

Укрощение случайности путем построения произвольных

В некоторых случаях вам может потребоваться указать, как создавать объекты нужного вам типа. Это особенно верно, если вы случайно используете объекты-значения, которые были определены как классы или структуры в C #. Здесь в игру вступает концепция произвольностей. Предположим, у вас есть очень простой класс, представляющий объект Currency. Хотя FsCheck может генерировать случайные строки и передавать их в качестве аргументов конструктору Currency, эти строки являются случайными, например '\\ X {} | X46s, что вполне может быть не тем значением, которое вы ожидали бы ввести в Currency. . Однако вы можете создать свой собственный произвольный файл и использовать его в FsCheck. Давайте посмотрим, как это делается.

Чтобы использовать свои собственные произвольные библиотеки, вы должны установить в поле «Произвольный» массив произвольных типов генераторов. Это классы, у которых есть статические методы с блоком сигнатуры - ›Arb‹ ’a›. Затем тест на основе свойств будет знать, где искать, когда обнаружит один из этих типов. Второе - это определение фактического произвольного.

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

  1. Мы знаем конечное число допустимых значений для выборки.
  2. FsCheck уже умеет генерировать случайные положительные целые числа.

Теперь мы сначала определяем массив действительных валют, а затем ограничиваем это случайное целое число, чтобы оно всегда попадало в действительный индекс нашего массива допустимых значений. Затем мы выбираем это значение из массива и передаем его в наш конструктор, который дает генератор, который дает нам случайные объекты Currency с данными из нашего допустимого диапазона валют. Наконец, из этого генератора делаем произвольный без прикрепленного к нему термоусадочного устройства.

Модуль Gen также имеет множество других функций, таких как фильтрация значений, выбор множества элементов из предоставленного генератора и т. Д. Но одна из самых важных вещей, которые нужно понимать в генераторах, - это то, что они хорошо компонуются. Это очень мощная концепция, поскольку, если вы или кто-то другой уже написали Арбитри для компонентов вашего нового класса, вы можете легко сгенерировать для него произвольные. Например, если вы хотите построить объекты случайного количества только с положительными суммами и действительными валютами, вы можете осуществить это с помощью приведенного ниже кода.

Обратите внимание, что тест на основе свойств полагается только на знание о существовании типа AmountArb. Также обратите внимание, что AmountArb и CurrencyArb - это два разных типа. Вам не обязательно иметь все ваши Arbitraries как методы в одном классе. При построении случайных сумм мы используем оператор фильтрации в генераторе десятичных чисел, чтобы гарантировать, что мы получаем только положительные случайные значения, и мы получаем экземпляр валюты Arbitrary и извлекаем из него генератор. Наконец, мы объединяем два генератора, используя метод Gen.map2. Для этого требуется функция, которая знает, как объединить выходные данные двух генераторов, и все, что нам нужно сделать, это передать аргументы нашему конструктору Amount. Это дает нам произвольный объект, который знает, как создавать объекты Amount.

Приемочные испытания на основе собственности

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

Теперь предположим, что в вашем коде есть черный ящик, для которого вам нужно написать замену. Это может быть либо конечная точка для службы, либо DLL, связанная с вашей программой, исходный код которой был утерян, и т. Д. Здесь свойство, которое вы хотите проверить, состоит в том, что для случайных входов ваша реализация дает то же неизвестная эталонная реализация. Если служба является внешней, вы вполне можете убедиться, что вы не используете DDoS-атаки с помощью удаленных вызовов при каждом запуске ваших тестов.

Другие соображения при тестировании со свойствами

Тесты на основе свойств полезны, но не панацея. Их может быть немного сложнее читать другим разработчикам, поддерживающим кодовую базу, которые не привыкли проводить тестирование на основе свойств. Когда вы решите реализовать свои собственные произвольные библиотеки и ограничите возможные значения, которые может принимать рандомизатор, вы рискуете упустить важные случаи из-за чрезмерной строгости, как при написании модульных тестов. Тесты на основе свойств также работают медленнее, чем модульные тесты, по умолчанию они генерируют 100 случайных входных данных и тестируют их. Если у вас есть много тестов, основанных на свойствах, или вы запускаете случайные входные данные для некоторых из них, ваш опыт работы с параллельными инструментами тестирования, такими как NCrunch, может ухудшиться. Поскольку вы имеете дело со случайностью, это также означает, что очень маловероятно, что два тестовых прогона будут полностью идентичными. У вас может быть один проход, а затем один запуск завершится неудачно. Если второй прогон выявит реальную проблему, все в порядке. Вам следует написать специальный модульный тест для этого случая, а затем исправить проблему. Однако, если это связано с тем, что один из ваших Arbitraries генерирует значения, которые сильно отличаются от ожидаемых входных данных, может быть труднее определить и исправить проблему. Это связано с тем, что вам сначала нужно идентифицировать Произвольный, дающий неожиданные значения, и заключить его в механизм фильтрации. Хотя это действительно возможно, это все же больше работы, чем просто изменение постоянного значения, определенного в модульном тесте.

Заключение

Тестирование на основе свойств - очень полезный инструмент, о котором часто забывают. С профессиональной точки зрения, я довольно много раз видел, что после введения тестов на основе свойств для проблемных частей кодовой базы он очень быстро выявлял иногда тонкие проблемы и продвигал эту часть кодовой базы с одного из обычных подозреваемых - в одно из последних мест, куда нужно смотреть. Однако к этому нужно привыкнуть, и я надеюсь, что эта статья помогла пробудить ваш интерес и преодолеть первоначальное препятствие на пути к тестированию на основе свойств. Для получения дополнительной информации, стоит взглянуть на документацию FsCheck. В этой статье в основном использовались примеры кода на F #, но FsCheck также можно использовать и на C #. Не стесняйтесь сообщить мне, если вы хотите увидеть статью, посвященную использованию FsCheck в C #.