开发者

How can I get Factory Girl to NEVER hit the database if I am calling Factory.build in order to make my controller tests FAST?

I am on a quest to make my Rails tests faster. I only have 520 tests, but they take 62 seconds to run in bash, and 82 seconds to run in Rubymine.

As an example of a typical controller test, I was using this code to sign_in as a @user and create the basic @comment in a CommentsController for my RSpec controller tests:

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

  @comment = Factory.create(:comment)
end

As you might realize... this is slow. It builds a @user, but also builds the associations for that user. Same for the @comment.

So I thought calling Factory.build(:user) would solve it... but I get weird errors. For example, current_user returns nil.

So... I decided to use Factory.build() and stub out all the before filters in my parent controller. However, my rspec log still says a TON of inserts are hitting the database when I inspect the RSPec log afterwards (we are talking hundreds of lines of code for just 3 tests!)

  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

The sad fact is, the above before(:each) block has ZERO effect on test performance. As I discovered, calling Factory.build() will still internally call Factory.create() on the child associations.

Here is a before(:each) block that effectively removes the junk produced in the RSpec log. It gave me a 35-40% test performance boost

  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

This makes the tests run faster, but it's also ugly as sin. We can't seriously write this for every test... do we? That's nuts. I'm not doing it.

I also want to point out that any one of these Factory.build() lines still takes about .15 seconds even though they are NOT hitting the database!

Running only 3 tests still results in about .3 to .35 seconds of time taken up by factory_girl PER test! I think that is totally unacceptable. If you remove the Factory.build() lines, the tests run in 0.00001 seconds.

I think the jury is in: factory_girl is one really slow library. Is the only solution to not use it?

Here is my 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 "N开发者_如何学C7G-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

And interesting benchmark. It takes .1 to .14 seconds on average to make a call to 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)

Even a Factory.build(:user) takes forever... and this is with :default_strategy => :build turned on!

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

Clearly this is evidence that something is wrong with factory_girl. The solution is to get rid of it or make sure it's using Factory.build. That is the answer.

Since I have basically solved my own problem, I am wondering why Factory_girl is so popular, and why is it "common wisdom"? One can objectively conclude that whatever benefits may be gained by using Factory Girl - and there's a lot of nice things about it - it is not worth the performance cost. I am sure a better factory gem could be developed that is much more performant... but factory_girl is unfortunately and regretfully not it.

My solution below uses basic object instantiation and stubs, and the tests continue to pass. I think using basic Ruby, stubs and filling in the object values manually on a per-test basis is the 'right' thing to do if you want to avoid fixtures and also get high performance when running tests.


Well, I guess I will answer my own question. I think it's the right answer, and maybe others can learn from it as I had to spend a few hours to learn it.

Here's how I got a 2000% (or 20x) speed improvement:

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

The solution is simply not to use Factories of any kind for controller tests (and perhaps other kinds of tests). I suggest only use Factory's when it is too much of a pain in the ass to do otherwise.

All 3 tests now run in 0.07 seconds! Before it was 1.4 seconds to run all 3 tests.

Factory_girl is simply a terribly slow library. I don't know what the heck it is doing, but it is not profiled properly.

Yes, I know it's doing a lot more than simple MyClass.new statements... but even for a slower scripting language like Ruby, the performance is many orders of magnitude slower than basic class instantiation. It needs to undergo some massive optimization so that Factory.build(:my_class) is brought more in line with MyClass.new

I would suggest to the implementers of Factory_girl to try and get it so that it's overhead is not much slower than a basic MyClass.new call (excluding database overhead... that can't be avoided). It should provide a nice way to build objects and you shouldn't have to pay a 20x performance penalty to get this benefit. That's not an acceptable trade-off.

This is all really too bad, because Factory.build would be nice in controllers when you have render_views turned on inside of your controller specs. There should be significant motivation to correct this.

In the meantime, just use basic Ruby/Rails classes. I think you'll be amazed how fast they actually are....


I had the same problem as @FireEmblem and eventually narrowed the issue down to FactoryGirl.build. FactoryGirl.stub didn't make things any better.

I finally realized it's because one of my models had validation logic that made an HTTP request when a certain field was present. The factory put a value in that field, so on the outside, it looked like FactoryGirl was slowing down my tests. In reality, it was, but only because it triggered the HTTP request. Removing one line from one of my factories eliminated the HTTP request, causing a 60x performance improvement.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜