Это плохая практика для случайного генерирования тестовых данных? - PullRequest
6 голосов
/ 12 марта 2009

Поскольку я начал использовать 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, я понял бы, что что-то не так. Если бы я использовал только приборы, обнаружение такой ошибки могло бы занять гораздо больше времени.

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

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

Ответы [ 12 ]

14 голосов
/ 12 марта 2009

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

5 голосов
/ 12 марта 2009

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

(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 , Машинист или FixtureReplacement .

5 голосов
/ 12 марта 2009

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

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

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

2 голосов
/ 12 марта 2009

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

1 голос
/ 12 марта 2009

Я бы посоветовал взглянуть на машиниста:

http://github.com/notahat/machinist/tree/master

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

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

1 голос
/ 12 марта 2009

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

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

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

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% времени.

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

1 голос
/ 12 марта 2009

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

0 голосов
/ 14 февраля 2012

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

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

0 голосов
/ 12 марта 2009

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

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

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

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

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

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

0 голосов
/ 12 марта 2009

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

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

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

...