Как я могу получить Factory Girl, чтобы НИКОГДА не попал в базу данных, если я вызываю Factory.build, чтобы мои контрольные тесты FAST?
Я пытаюсь сделать тесты Rails быстрее. У меня всего 520 тестов, но они занимают 62 секунды для запуска в bash и 82 секунды для запуска в Rubymine.
В качестве примера типичного теста контроллера я использовал этот код для sign_in как @user и создал базовый @comment в контроллере комментариев для тестов контроллера 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()
и вырезать все фильтры перед моим родительским контроллером. Тем не менее, мой журнал rspec все еще говорит, что TON вставок попадает в базу данных, когда я проверяю журнал 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)
имеет эффект ZERO на производительность теста. Как я выяснил, вызов 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()
по-прежнему занимает около 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 '[email protected]'
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
И интересный бенчмарк. Обычно требуется от 0,1 до 0,14 секунды, чтобы позвонить на Factory.create(:user)
:
$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.create(:user) } } }'
user system total real
9.940000 0.080000 10.020000 ( 14.872736)
Даже a 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, который будет намного более реалистичным... но, к сожалению, заводская девушка и, к сожалению, не она.
В моем решении ниже используются базовые экземпляры объектов и заглушки, и тесты продолжают проходить. Я думаю, что использование базового Ruby, заглушек и заполнение значений объектов вручную на основе каждого теста - это "правильная" вещь, если вы хотите избежать приспособлений, а также получить высокую производительность при выполнении тестов.
Ответы
Ответ 1
Ну, я думаю, я отвечу на свой вопрос. Я думаю, это правильный ответ, и, возможно, другие могут учиться на нем, поскольку мне пришлось потратить несколько часов, чтобы изучить его.
Вот как я получил улучшение скорости на 2000% (или 20x):
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
Решение просто не использовать фабрики любого типа для тестов контроллера (и, возможно, других видов тестов). Я предлагаю использовать только Factory, когда это слишком большая боль в заднице, чтобы сделать иначе.
Все 3 теста теперь выполняются через 0,07 секунды! До того, как это было 1,4 секунды, чтобы запустить все 3 теста.
Factory_girl - просто ужасно медленная библиотека. Я не знаю, что он делает, но он не профилирован должным образом.
Да, я знаю, что это делает намного больше, чем простые инструкции MyClass.new
... но даже для более медленного языка сценариев, такого как Ruby, производительность на много порядков медленнее, чем базовая реализация класса. Он должен подвергнуться некоторой массовой оптимизации, так что Factory.build(:my_class)
приводится в соответствие с MyClass.new
Я бы предложил разработчикам Factory_girl попробовать и получить его так, чтобы накладные расходы были не намного медленнее, чем базовый вызов MyClass.new
(исключая издержки базы данных... чего нельзя избежать). Он должен обеспечить хороший способ создания объектов, и вам не нужно платить штраф за 20 очков, чтобы получить эту выгоду. Это не приемлемый компромисс.
Это все очень плохо, потому что Factory.build
будет приятным в контроллерах, если вы включили render_views
внутри своих спецификаций контроллера. Должна быть важная мотивация, чтобы исправить это.
Тем временем просто используйте базовые классы Ruby/Rails. Я думаю, вы будете удивлены, насколько быстро они на самом деле...
Ответ 2
У меня была та же проблема, что и @FireEmblem, и в конечном итоге сузила проблему до FactoryGirl.build
. FactoryGirl.stub
не улучшал ситуацию.
Я, наконец, понял это, потому что у одной из моих моделей была логика проверки, которая сделала HTTP-запрос, когда какое-то поле присутствовало. factory поместите значение в это поле, так что снаружи это выглядело так, как FactoryGirl замедлял мои тесты. На самом деле это было, но только потому, что оно вызвало HTTP-запрос. Удаление одной строки с одной из моих фабрик устранило HTTP-запрос, что привело к повышению производительности на 60%.