Вспомните, когда вы в последний раз писали модульные тесты (надеюсь, это довольно недавно). Вы должны были придумать счастливый путь, трагический путь и эти труднодоступные крайние случаи. Поскольку мы не непогрешимые люди, мы склонны пропускать вещи чаще, чем нет. Мы пропускаем крайний случай здесь, забываем об обработке ошибки там и обнаруживаем наши упущения во время производства.
Тестирование на основе свойств полностью перевернуло понятие модульного тестирования с ног на голову. Вместо того, чтобы писать конкретные примеры теста, вы должны написать свойство . Свойство в этом контексте означает условие, которое будет выполняться для всех заданных вами входных данных.
Пример здесь, безусловно, полезен, если не требуется. Допустим, вы хотите протестировать функцию, которая переворачивает массив. Какие могут быть полезные свойства? Ну, я могу думать о следующем:
- Первый элемент массива становится последним элементом
- Длина массива остается неизменной до и после обращения
- Обращение массива дважды вернет исходный массив
Возможно, вы могли бы придумать еще несколько. Используя инструмент тестирования, основанный на свойствах, вы можете выразить вышеприведенное в виде кода и сказать ему генерировать сотни или даже тысячи тестовых случаев со случайно сгенерированными массивами.
Крутая особенность многих инструментов тестирования на основе свойств сокращается. Это означает, что инструмент в меру своих возможностей найдет наименьшее входное значение, которое приводит к неудовлетворенности свойства.
Это звучит знакомо …
Если приведенное выше описание звучит знакомо, возможно, вы слышали об инструменте тестирования под названием QuickCheck. QuickCheck изначально был написан на Haskell , но нашел свой путь в таких языках, как Erlang . Существуют и другие реализации QuickCheck, такие как ScalaCheck для Scala и FsCheck для F #.
Хотя вышеперечисленные языки в основном являются функциональными языками, это не помешало создателям Rantly создать инструмент, подобный QuickCheck для Ruby, вместе с симпатичным именем.
В этой статье мы расширим наш тестовый репертуар, попробовав тестирование на основе свойств в Ruby.
Настройка Rantly с RSpec
Сначала мы настроим демонстрационный проект для использования Rantly с RSpec. Инструкции аналогичны для MiniTest и TestUnit. Давайте создадим демонстрационный проект:
% mkdir rantly-demo && cd rantly-demo
Затем создайте скелет Gemfile :
% bundle init Writing new rantly-demo/Gemfile
Нам нужно только rantly
source "https://rubygems.org" gem "rantly"
Время установить зависимости:
% bundle Fetching gem metadata from https://rubygems.org/.. Fetching version metadata from https://rubygems.org/. Resolving dependencies... Using rantly 0.3.2 Using bundler 1.10.6
Предполагая, что вы уже установили rspec
, вы можете быстро настроить свой проект на использование RSpec:
rspec --init
Используя Rantly
Вот как бы вы выразили свойство Rantly:
it "define your property here" do property_of { Rantly { <GENERATOR GOES HERE> } }.check { |a_generated_value| <EXPECTATION GOES HERE> } end
Таким образом, пример будет:
it "integer property only returns Integer type" do property_of { integer # the generator }.check { |i| # i is the generated value expect(i).to be_a(Integer) # the expectation } end
Неудачный тест и сокращение
Давайте посмотрим, что Rantly говорит нам, когда свойство терпит неудачу. Мы скажем Rantly создать массивы целых чисел, а затем проверить, что каждый сгенерированный массив имеет полностью четные элементы. Очевидно, что это не удастся. Интересно, как это потерпит неудачу? Давайте напишем теневое свойство в spec / array_spec.rb :
require 'rantly' require 'rantly/rspec_extensions' require 'rantly/shrinks' RSpec.describe "Array" do it "even numbers" do property_of { Rantly { array { integer } } }.check { |i| expect(i).to all(be_even) } end end
Когда вы запускаете файл, это вывод:
[0, 0, 0, 0, -1324248444, -819907805037675589] found a reduced failure case: ... [0, 0, 0, 0, -10102, -819907805037675589] found a reduced failure case: ... [0, 0, 0, 0, -77, -819907805037675589] found a reduced failure case: ... [0, 0, 0, 0, -1, -819907805037675589] found a reduced failure case: ... minimal failed data is: [0, 0, 0, 0, 0, -819907805037675589] F Failures: 1) Array even numbers Failure/Error: expect(i).to all(be_even) expected [-1384706466568309853, -2143298094606122148, 2181188094126790798, 1908884087348911076, -710950470620772656, -819907805037675589] to all be even object at index 0 failed to match: expected `-1384706466568309853.even?` to return true, got false object at index 5 failed to match: expected `-819907805037675589.even?` to return true, got false
Здесь мы видим, что Rantly пытается найти наименьший случай отказа, уменьшив количество элементов до минимально возможного. Этот процесс называется сокращением . Rantly может выполнять сжатие целых чисел, строк, массивов и хэшей.
К сожалению, это не уменьшило последний элемент. Однако, относительно очевидно (после некоторого прищуривания), что -819907805037675589
— отрицательное число.
Придя со свойствами
Безусловно, самая сложная задача при тестировании на основе свойств заключается в том, чтобы сначала создать свойства. В этом разделе мы рассмотрим некоторые полезные методы, чтобы выяснить, какие свойства писать. Этот список не является исчерпывающим, но служит хорошей отправной точкой.
1. Обратные функции
Обычно это довольно очевидно. Примеры обратных функций включают в себя:
- Кодирование и декодирование (например, Base64)
- Сериализация и десериализация (например, JSON)
- Добавление и удаление
Хотя написание свойства, которое использует «обратное значение», на самом деле не охватывает много, это свойство чрезвычайно полезно для проверки работоспособности. Соедините это с сотней или более сгенерированных тестовых случаев, и вы должны чувствовать себя довольно уверенно.
Вот пример того, как проверить кодирование и декодирование Base 64. Создайте base64_spec.rb в спецификации :
require 'rantly' require 'rantly/rspec_extensions' require 'rantly/shrinks' require 'base64' RSpec.describe "Base64" do it "encoding and decoding are inverses of each other" do property_of { Rantly { sized(30) { string } } }.check(1000) { |s| puts s expect(Base64.decode64(Base64.encode64(s))).to eq(s) } end end
Это в основном создает случайные 30-символьные строки и генерирует 1000 тестов, утверждая, что кодирование и декодирование действительно противоположны друг другу. Вот выборка:
`` ... IF@5!x}PI({m[8XPw=r1Vep(\*uIi Cz)ZkAcUUE],xoOI/@g*&;
I): JVN $
J \ Oo ”PT R-8[A3);k*5Li0+v;[e8o= R{IL2Vz]$.KcOG<uy<gBpPc}T|+j7n .gsw?:?#"Iy%0O>-V0!]y#K}
R-8[A3);k*5Li0+v;[e8o= R{IL2Vz]$.KcOG<uy<gBpPc}T|+j7n .gsw?:?#"Iy%0O>-V0!]y#K}
R-8[A3);k*5Li0+v;[e8o= R{IL2Vz]$.KcOG<uy<gBpPc}T|+j7n .gsw?:?#"Iy%0O>-V0!]y#K}
6> M! Ny
xnkLFCLeim) VR9r | qaZuoYrNWd1GOU
? ~ Т | 6 ;; Н ~ термометр)
Kb [Или V’ypYI KwK> хв>:!? S- D GXS
! Фунт% мт> u0V6xHwh9 + D6C $ / Q0! УКМ \ Y
успех: 1000 тестов
,
«`
2. Идемпотентность
Идемпотент — слово, чтобы произвести впечатление на ваших друзей и раздражать ваших коллег! Это означает, что делать это один раз — это то же самое, что делать это несколько раз. Например, вызов Array#uniq
несколько раз приведет к одному и тому же значению. Функции сортировки также относятся к той же категории.
Давайте попробуем Array#uniq
(вы можете поместить это в spec / uniq.rb ):
require 'rantly' require 'rantly/rspec_extensions' require 'rantly/shrinks' RSpec.describe "Array" do it "uniq is idempotent" do property_of { Rantly { array { Rantly { i = integer; guard i >= 0; i } } } }.check { |a| expect(a.uniq.uniq).to eq(a.uniq) } end end
Здесь мы генерируем массив неотрицательных целых чисел. Мы используем генератор защиты, чтобы ограничить сгенерированные целые числа только неотрицательными:
Rantly { i = integer; guard i >= 0; i }
3. Использование существующей реализации
Допустим, вы обнаружили новый алгоритм сортировки QuickerSort, который, как вы знаете, работает быстрее, чем существующий алгоритм сортировки, реализованный в Ruby. Теперь все, что вам нужно сделать, это убедиться, что ваша реализация QuickerSort дает те же результаты, что и реализация Ruby.
С помощью QuickCheck мы можем легко выразить свойство следующим образом:
RSpec.describe "Array" do it "Array#quicker_sort works produces the same result as Array#sort" do property_of { Rantly { array(range(0, 100)) { integer }} }.check { |a| expect(a.quicker_sort).to eq(a.sort) } end end
Здесь мы генерируем массив случайных целых чисел, который может быть пустым массивом вплоть до массива из 100 элементов.
Пользовательские генераторы: создание последовательности ДНК
Давайте узнаем, как создать собственный генератор. Пользовательские генераторы полезны, когда ваши входные данные должны соответствовать определенным требованиям. Например, если метод работает только с двоичными цифрами, использование целочисленного генератора по умолчанию не будет очень полезным.
В этом примере мы создадим генератор последовательности ДНК. Для наших целей последовательность ДНК в основном представляет собой массив, который содержит комбинацию A
, T
, G
и C
Примером может быть ["C", "G", "A", "G", "A", "T", "G"]
. Наша первая остановка — Rantly#choose
, который позволяет генератору выбрать значение из указанных вариантов:
choose("A", "T", "G", "C")
Далее мы знаем, что нам нужен массив. Генератор массива принимает блок, который вызывается для генерации элемента массива. Это именно то, что нам нужно:
Rantly { array { choose("A", "T", "G", "C") } }
Чтобы добавить некоторые вариации, мы также можем заставить генератор создавать массивы различной длины, указав диапазон:
Rantly { array(range(0, 20)) { choose("A", "T", "G", "C") } }
Попробуйте это на консоли. Вам нужно будет выполнить require "rantly"
:
> 10.times { p Rantly { array(range(1,20)) { choose("A", "T", "G", "C") } } } ["T", "A", "A", "G", "A", "A", "T", "G", "G", "T", "A", "T", "T", "T"] ["T", "A", "T", "T", "G"] ["T", "T", "C", "G", "T", "T", "C", "A"] ["T", "C", "C"] ["G", "G", "T", "C"] ["A", "A", "A", "A", "C", "G", "T", "G", "G", "T"] ["C", "A", "G"] ["T", "G", "C", "C", "A", "C", "C", "T", "G", "C", "T", "C", "G", "C"] ["G", "C", "T", "T", "T", "A", "C", "A"] ["G", "G", "G", "A", "C", "T", "G", "C"] => 10
Довольно круто, а?
Резюме
Тестирование на основе свойств предлагает другой способ думать о тестах. Вместо того, чтобы писать конкретные примеры, почему бы не написать общие свойства и позволить инструменту генерировать тестовые случаи для вас?
Однако, пока не выбрасывайте свои юнит-тесты! Основанное на свойствах тестирование, вероятно, было бы наиболее полезным для тестирования таких вещей, как структуры данных, функции без сохранения состояния (то есть функции в смысле слова функционального программирования) и алгоритмы. Это, вероятно, не очень подходит для тестирования бизнес-логики. Другими словами, Rantly — это новый инструмент в вашем поясе, а не весь ремень.
Попробуйте Rantly и дайте мне знать, как это происходит. Удачного тестирования!