Является ли это плохой практикой генерировать тестовые данные случайным образом?

StackOverflow https://stackoverflow.com/questions/636353

  •  10-07-2019
  •  | 
  •  

Вопрос

С тех пор, как я начал использовать rspec, у меня возникла проблема с понятием фикстур.Мои основные опасения заключаются в следующем:

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

  2. Я использую тестирование как форму документации кода.Если у меня есть жестко закодированные значения параметров, трудно определить, что пытается продемонстрировать конкретный тест.Например:

    describe Item do
      describe '#most_expensive' do
        it 'should return the most expensive item' do
          Item.most_expensive.price.should == 100
          # OR
          #Item.most_expensive.price.should == Item.find(:expensive).price
          # OR
          #Item.most_expensive.id.should == Item.find(:expensive).id
        end
      end
    end
    

    Использование первого метода не дает читателю никакой информации о том, какой предмет является самым дорогим, а только то, что его цена равна 100.Все три метода требуют от читателя принять на веру, что приспособление :expensive является самым дорогим из перечисленных в fixtures/items.yml.Неосторожный программист может нарушить тесты, создав Item в before(:all), или вставив другое приспособление в fixtures/items.yml.Если это большой файл, может потребоваться много времени, чтобы выяснить, в чем проблема.

Одна вещь, которую я начал делать, это добавить #generate_random метод для всех моих моделей.Этот метод доступен только тогда, когда я запускаю свои спецификации.Например:

class Item
  def self.generate_random(params={})
    Item.create(
      :name => params[:name] || String.generate_random,
      :price => params[:price] || rand(100)
    )
  end
end

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

describe Item do
  describe '#most_expensive' do
    before(:all) do
      @items = []
      3.times { @items << Item.generate_random }
      @items << Item.generate_random({:price => 50})
    end

    it 'should return the most expensive item' do
      sorted = @items.sort { |a, b| b.price <=> a.price }
      expensive = Item.most_expensive
      expensive.should be(sorted[0])
      expensive.price.should >= 50      
    end
  end
end

Таким образом, мои тесты лучше выявляют неожиданное поведение.Когда я генерирую данные таким образом, я иногда натыкаюсь на крайний случай, когда мой код ведет себя не так, как ожидалось, но который я бы не уловил, если бы использовал только фикстуры.Например, в случае #most_expensive, если бы я забыл обработать особый случай, когда несколько товаров имеют самую дорогую цену, мой тест иногда терпел бы неудачу с самого начала. should.Увидев недетерминированные сбои в AutoSpec, я понял, что что-то не так.Если бы я использовал только фикстуры, обнаружение такой ошибки могло бы занять гораздо больше времени.

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

Итак, это плохая практика?Является ли мой страх перед приборами иррациональным?Пишет generate_random метод для каждой модели слишком много работы?Или это работает?

Это было полезно?

Решение

Это ответ на ваш второй пункт:

  

(2) Я использую тестирование как форму документации для кода. Если у меня есть жестко запрограммированные значения приборов, трудно выявить, что пытается продемонстрировать конкретный тест.

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

Из-за этого многие пользователи RSpec вообще прекратили использовать приборы. Вместо этого создайте необходимые объекты в самом примере спецификации.

describe Item, "#most_expensive" do
  it 'should return the most expensive item' do
    items = [
      Item.create!(:price => 100),
      Item.create!(:price => 50)
    ]

    Item.most_expensive.price.should == 100
  end
end

Если у вас много шаблонного кода для создания объекта, вам следует взглянуть на некоторые из множества библиотек фабрики тестовых объектов, например, factory_girl , Машинист или < a href = "http://replacefixtures.rubyforge.org/" rel = "nofollow noreferrer"> FixtureReplacement .

Другие советы

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

Мы много думали об этом в моем недавнем проекте.В итоге мы остановились на двух моментах:

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

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

Много полезной информации уже опубликовано, но см. также: Fuzz Testing . По слухам, Microsoft использует этот подход во многих своих проектах.

Мой опыт тестирования в основном связан с простыми программами, написанными на C / Python / Java, поэтому я не уверен, что это полностью применимо, но всякий раз, когда у меня есть программа, которая может принять любой пользовательский ввод, я всегда включаю тест со случайными входными данными или, по крайней мере, входными данными, сгенерированными компьютером непредсказуемым образом, потому что вы никогда не сможете сделать предположения о том, что будут вводить пользователи. Или, ну, вы можете , но если вы это сделаете, то какой-нибудь хакер, который не делает такого предположения, вполне может найти ошибку, которую вы полностью пропустили. Машинно-генерируемые входные данные - лучший (единственный?) Способ, которым я знаю, чтобы полностью исключить предвзятость человека из процедур тестирования. Конечно, чтобы воспроизвести проваленный тест, вы должны сделать что-то вроде сохранения тестового ввода в файл или распечатать его (если это текст) перед запуском теста.

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

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

Затем вы переходите от случайного тестирования к статистическому.

if (a > 0)
    // Do Foo
else (if b < 0)
    // Do Bar
else
    // Do Foobar

Если вы случайно выберете a и b в диапазоне int , вы выполните Foo в 50% случаев. , Bar 25% времени и Foobar 25% времени. Вероятно, вы найдете больше ошибок в Foo , чем в Bar или Foobar .

Если вы выберете a так, чтобы оно было отрицательным в 66,66% случаев, Bar и Foobar выполняются больше, чем при первом распространении , Действительно, три ветви выполняются каждый 33,33% времени.

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

Одна проблема со случайно сгенерированными тестовыми примерами состоит в том, что проверка ответа должна быть вычислена кодом, и вы не можете быть уверены, что в ней нет ошибок:)

Эффективность такого тестирования во многом зависит от качества используемого вами генератора случайных чисел и от того, насколько правильным является код, который переводит выходные данные ГСЧ в тестовые данные.

Если ГСЧ никогда не выдает значения, из-за которых ваш код попадает в какое-либо граничное условие, этот случай не будет охвачен. Если ваш код, который переводит выходные данные RNG во входные данные тестируемого кода, неисправен, может случиться так, что даже с хорошим генератором вы все равно не достигнете всех крайних случаев.

Как вы будете проверять это?

Проблема со случайностью в тестовых случаях заключается в том, что выходные данные, в общем, случайные.

Идея тестов (особенно регрессионных) заключается в том, чтобы убедиться, что ничего не сломано.

Если вы обнаружите что-то сломанное, вам нужно будет включать этот тест каждый раз, иначе у вас не будет согласованного набора тестов. Кроме того, если вы запускаете случайный тест, который работает, вам нужно включить этот тест, потому что возможно, что вы можете нарушить код, чтобы тест не прошел.

Другими словами, если у вас есть тест, который использует случайные данные, сгенерированные на лету, я думаю, что это плохая идея. Однако, если вы используете набор случайных данных, КОТОРЫЙ ВЫ ХРАНИТЕ И ПОЛЬЗУЕТЕ, это может быть хорошей идеей. Это может принять форму набора начальных чисел для генератора случайных чисел.

Это хранение сгенерированных данных позволяет вам найти «правильный» ответ на эти данные.

Итак, я бы порекомендовал использовать случайные данные для исследования вашей системы, но использовать определенные данные в ваших тестах (которые могли изначально быть случайными данными)

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

Я настоятельно рекомендую использовать Factory Girl и ffaker для этого. (Никогда не используйте светильники Rails ни при каких обстоятельствах.)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top