Как я могу заставить Factory Girl НИКОГДА не обращаться к базе данных, если я вызываю Factory.build, чтобы мои контроллеры тестировались БЫСТРО? - PullRequest
21 голосов
/ 25 мая 2011

Я стремлюсь ускорить мои тесты Rails. У меня только 520 тестов, но они требуют 62 секунд для запуска в bash и 82 секунды для запуска в Rubymine.

В качестве примера типичного теста контроллера я использовал этот код для входа в систему в качестве @user и создания базового @comment в CommentsController для моих тестов контроллера RSpec:

before(:each) do
  @user = Factory.create(:user)
  sign_in @user

  @comment = Factory.create(:comment)
end

Как вы понимаете ... это медленно. Он создает @user, но также создает ассоциации для этого пользователя. То же самое для @comment.

Так что я думал, что звонить по номеру Factory.build(:user) это решит ... но я получаю странные ошибки. Например, current_user возвращает nil.

Итак ... Я решил использовать Factory.build() и отключить все фильтры before в моем родительском контроллере. Тем не менее, мой журнал rspec все еще говорит, что ТОНА вставок попадает в базу данных, когда я проверяю журнал RSPec впоследствии (мы говорим о сотнях строк кода всего за 3 теста!)

  before(:each) do
    @user = Factory.build(:user)
    #sign_in @user

    controller.stub(:authenticate_user!) #before_filter
    controller.stub(:add_secure_model_data) #before_filter
    controller.stub(:current_user).and_return(@user)

    @comment = Factory.build(:comment)
  end

Печальный факт заключается в том, что указанный выше блок before(:each) оказывает нулевое влияние на производительность теста. Как я обнаружил, вызов Factory.build() все равно вызовет Factory.create() внутри дочерних ассоциаций.

Вот блок before(:each), который эффективно удаляет ненужные файлы, созданные в журнале RSpec. Это дало мне повышение производительности теста на 35-40%

  before(:each) do
    @user = Factory.build(:user, :role => Factory.build(:role))
    #sign_in @user

    controller.stub(:authenticate_user!)
    controller.stub(:add_secure_model_data)
    controller.stub(:current_user).and_return(@user)

    # both of these are still super slow. WTF?!
    @site_update = Factory.build(:site_update, :id => 5, :author => Factory.build(:user, :role => Factory.build(:role)))

    @comment = Factory.build(:comment,
                             :author => Factory.build(:user, :role => Factory.build(:role)),
                             :commentable => @site_update)
  end

Это заставляет тесты работать быстрее, но это также ужасно, как грех. Мы не можем серьезно написать это для каждого теста ... не так ли? Это безумие Я этим не занимаюсь.

Я также хочу отметить, что любая из этих Factory.build() строк по-прежнему занимает около 0,15 секунды, даже если они НЕ попадают в базу данных!

Выполнение только 3 тестов все еще приводит к от 0,3 до 0,35 секунд времени, затраченного на тест factory_girl PER! Я думаю, что это совершенно неприемлемо. Если удалить строки Factory.build(), тесты будут выполняться через 0,00001 секунды.

Я думаю, что присяжные заседатели: factory_girl - это одна очень медленная библиотека. Это единственное решение не использовать его?

Вот мой factories.rb:

Factory.define :role do |f|
  f.name "Admin"
end

Factory.define :user do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "Banoo.Smith#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :admin do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "admin#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :course_provider do |f|
  f.first_name "Josh"
  f.last_name "Bolson"
  f.sequence(:email) { |n| "josh.bolson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :director do |f|
  f.first_name "Director"
  f.last_name "Dude"
  f.sequence(:email) { |n| "director#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :instructor do |f|
  f.first_name "Instructor"
  f.last_name "Dude"
  f.sequence(:email) { |n| "instructor#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :trainee do |f|
  f.first_name "Trainee"
  f.last_name "Dude"
  f.sequence(:email) { |n| "trainee#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :private_message do |f|
  f.subject "Subject"
  f.content "content"
  f.is_deleted_by_sender false
  f.association :sender, :factory => :user
end

Factory.define :recipient do |f|
  f.is_read false
  f.is_deleted false
  f.association :receiver, :factory => :user
  f.association :private_message
end

Factory.define :course_template do |f|
  f.name "name"
  f.description "description"
  f.association :course_provider
end

Factory.define :site_update do |f|
  f.subject "Subject"
  f.intro "intro"
  f.content "content"
  f.association :author, :factory => :user
end

Factory.define :comment do |f|
  f.content "content"
  f.association :author, :factory => :user
  f.association :commentable, :factory => :site_update
end

Factory.define :country do |f|
  f.name "Liberty"
end

Factory.define :province do |f|
  f.name "Freedom"
  f.association :country
end

Factory.define :payment_plan do |f|
  f.name "name"
  f.monthly_amount 79
  f.audience "Enterprises"
  f.active_courses "500-2000"
end

Factory.define :company do |f|
  f.name "name"
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N7G-5F4"
  f.association :province
  f.association :payment_plan
end

Factory.define :company_user do |f|
  f.first_name "Dan"
  f.last_name "Grayson"
  f.sequence(:email) { |n| "dan.grayson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
  f.association :company
end

Factory.define :course do |f|
  f.notes "notes"
  f.difficulty 100
  f.association :course_template
  f.association :instructor, :factory => :company_user
end

Factory.define :study_group do |f|
  f.name "name"
end

Factory.define :help_category do |f|
  f.name "name"
end

Factory.define :help_document do |f|
  f.question "question"
  f.content "content"
  f.association :category, :factory => :help_category
end

Factory.define :tag do |f|
  f.name "name"
end

Factory.define :partial_mapping do |f|
  f.from_suffix "ing"
  f.to_suffix "ing"
end

Factory.define :newsletter do |f|
  f.subject "subject"
  f.content "content"
end

Factory.define :press_contact do |f|
  f.full_name "Banoo Smith"
  f.email 'Banoo.Smith@gmail.com'
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N9B-3W5"
  f.association :province
end

Factory.define :press_release do |f|
  f.headline "Headline"
  f.origin "origin"
  f.intro "intro"
  f.body "body"
  f.association :contact, :factory => :press_contact
end

Factory.define :theme do |f|

end

И интересный тест. В среднем на вызов Factory.create(:user):

требуется от 0,1 до 0,14 секунды.
$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.create(:user) } } }' 
      user     system      total        real
  9.940000   0.080000  10.020000 ( 14.872736)

Даже Factory.build(:user) длится вечно ... и это при включенном :default_strategy => :build!

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.build(:user) } } }'
      user     system      total        real
  9.350000   0.030000   9.380000 ( 11.798339)

Очевидно, это свидетельство того, что что-то не так с factory_girl. Решение состоит в том, чтобы избавиться от него или убедиться, что он использует Factory.build. Это ответ.

Поскольку я в основном решил свою собственную проблему, мне интересно, почему Factory_girl так популярен и почему это "общая мудрость"? Можно объективно заключить, что любые выгоды, которые можно получить с помощью Factory Girl - и в этом есть много хороших вещей - не стоят затрат на производительность. Я уверен, что можно было бы разработать более качественную фабричную жемчужину, которая была бы намного более производительной ... но factory_girl, к сожалению и с сожалением, не такая.

Мое решение, приведенное ниже, использует базовые экземпляры объектов и заглушки, и тесты продолжают проходить. Я думаю, что использование базовых Ruby, заглушек и заполнение значений объектов вручную для каждого теста - это «правильная» вещь, которую нужно делать, если вы хотите избежать фикстур, а также добиться высокой производительности при выполнении тестов.

Ответы [ 2 ]

17 голосов
/ 26 мая 2011

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

Вот как я получил улучшение скорости на 2000% (или в 20 раз):

before(:each) do
  @user = User.new
  controller.stub(:authenticate_user!)
  controller.stub(:current_user).and_return(@user)
  controller.stub(:add_secure_model_data)

  @site_update = SiteUpdate.new
  @comment = Comment.new
end

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

Все 3 теста теперь выполняются за 0,07 секунды!До того, как пройти все 3 теста, потребовалось 1,4 секунды.

Factory_girl - просто ужасно медленная библиотека.Я не знаю, какого черта он делает, но он не профилирован должным образом.

Да, я знаю, что он делает гораздо больше, чем простые MyClass.new заявления ... но даже для более медленного языка сценариевкак и в Ruby, производительность на много порядков ниже, чем у базовых классов.Нужно провести массивную оптимизацию, чтобы Factory.build(:my_class) был приведен в большее соответствие с MyClass.new

. Я бы посоветовал разработчикам Factory_girl попытаться получить его так, чтобы его издержки были не намного медленнее, чембазовый вызов MyClass.new (исключая издержки базы данных ... этого нельзя избежать).Это должно обеспечить хороший способ создания объектов, и вам не нужно платить 20-кратное снижение производительности, чтобы получить это преимущество.Это неприемлемый компромисс.

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

А пока просто используйте базовые классы Ruby / Rails.Я думаю, вы будете удивлены, насколько быстро они на самом деле ....

0 голосов
/ 08 ноября 2013

У меня была та же проблема, что и у @FireEmblem, и в итоге я сузил проблему до FactoryGirl.build.FactoryGirl.stub не улучшил ситуацию.

Я наконец понял, что это потому, что одна из моих моделей имела логику проверки, которая выполняла HTTP-запрос при наличии определенного поля.Фабрика поместила значение в это поле, поэтому снаружи казалось, что FactoryGirl замедляет мои тесты.В действительности это было так, но только потому, что это вызвало HTTP-запрос.Удаление одной строки с одного из моих заводов исключило HTTP-запрос, что привело к повышению производительности в 60 раз.

...