如果我调用Factory.build以使我的控制器测试更快,我怎么能让Factory Girl永远不会打到数据库?

Fir*_*lem 21 ruby unit-testing controller ruby-on-rails devise

我正在努力让我的Rails测试更快.我只有520次测试,但他们需要62秒才能在bash中运行,而82秒则在Rubymine中运行.

作为典型控制器测试的一个例子,我使用此代码以@user身份登录,并在我的RSpec控制器测试的CommentsController中创建基本的@comment:

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

  @comment = Factory.create(:comment)
end
Run Code Online (Sandbox Code Playgroud)

你可能会意识到......这很慢.它构建了一个@user,但也为该用户构建了关联.同样的@comment.

所以我认为呼叫Factory.build(:user)可以解决它...但我得到了奇怪的错误.例如,current_user退货nil.

所以......我决定Factory.build()在父控制器中使用并删除所有之前的过滤器.但是,我的rspec日志仍然表示当我之后检查RSPec日志时,TON的插入数据库正在访问数据库(我们正在讨论数百行代码,仅用于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
Run Code Online (Sandbox Code Playgroud)

可悲的事实是,上述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
Run Code Online (Sandbox Code Playgroud)

这使得测试运行得更快,但它也像罪一样丑陋.我们不能为每次测试都认真地写这个...我们呢?那太疯狂了.我不是这样做的.

我还想指出,Factory.build()即使它们没有访问数据库,这些行中的任何一行仍然需要大约.15秒!

仅运行3次测试仍然会导致factory_girl PER测试花费大约.3到.35秒的时间!我认为这是完全不可接受的.如果删除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
Run Code Online (Sandbox Code Playgroud)

而有趣的基准.平均需要.1到.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)
Run Code Online (Sandbox Code Playgroud)

即使是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)
Run Code Online (Sandbox Code Playgroud)

很明显,这证明了factory_girl出了问题.解决方案是摆脱它或确保它正在使用Factory.build.这就是答案.

既然我基本上解决了自己的问题,我想知道为什么Factory_girl如此受欢迎,为什么它是"常识"呢?人们可以客观地得出结论,使用Factory Girl可以获得任何好处 - 并且有很多好处 - 它不值得性能成本.我相信可以开发出更好的工厂宝石,性能更高......但是不幸的是,factory_girl并不令人遗憾.

我的解决方案使用基本对象实例化和存根,测试继续通过.我认为使用基本的Ruby,存根并在每个测试的基础上手动填充对象值是"正确"的事情,如果你想避免灯具并在运行测试时也能获得高性能.

Fir*_*lem 17

好吧,我想我会回答我自己的问题.我认为这是正确的答案,也许其他人可以从中学习,因为我不得不花几个小时来学习它.

这是我如何获得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
Run Code Online (Sandbox Code Playgroud)

解决方案根本就是不使用任何类型的工厂来进行控制器测试(以及其他类型的测试).我建议只有使用工厂的时候才能做太多的痛苦.

所有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类.我想你会惊讶他们实际上有多快......

  • 将存根与任何数据库写入进行比较几乎不是一个公平的比较.如果比较工厂女孩所花费的时间,与手动编写的Rails'model.create`调用所花费的时间相比,差异很小.您的问题的根源是与`build`工厂调用关联的对象的构建策略是创建它们,从而导致DB调用.解决方案可能是不使用工厂进行协会. (3认同)
  • @Douglas F Shearer:我在问题中举了一个例子,它仅使用`Factory.build()`,并且永远不会触及数据库(通过查看我的RSpec日志进行验证)。出于所有目的和目的,它几乎和在mysql中创建记录的版本一样慢。击中数据库仅需承担约30%的费用。另外70%是由于factory_girl *膨胀*和*开销*造成的,您只需不使用它,就可以完全忽略factory_girl的成本。我现在在** 5 **秒内运行了250个控制器测试,几乎没有任何实际缺陷。 (2认同)