如果我调用 Factory.build 以使我的控制器测试快速,如何让 Factory Girl 永远不会访问数据库?
Posted
技术标签:
【中文标题】如果我调用 Factory.build 以使我的控制器测试快速,如何让 Factory Girl 永远不会访问数据库?【英文标题】: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? 【发布时间】:2011-05-25 17:33:48 【问题描述】:我正在努力提高我的 Rails 测试速度。我只有 520 个测试,但它们在 bash 中运行需要 62 秒,在 Rubymine 中运行需要 82 秒。
作为典型控制器测试的示例,我使用此代码以 @user 身份登录并在 CommentsController 中为我的 RSpec 控制器测试创建基本 @comment:
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 日志时,我的 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 次测试仍会导致 factory_girl PER 测试占用大约 0.3 到 0.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
和有趣的基准。呼叫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 可能获得的任何好处 - 并且有很多关于它的好处 - 不值得付出性能成本。我确信可以开发出更好的、性能更高的工厂 gem……但不幸的是,factory_girl 不是它。
我下面的解决方案使用基本的对象实例化和存根,并且测试继续通过。我认为,如果您想避免固定装置并在运行测试时获得高性能,我认为使用基本的 Ruby、存根并在每个测试的基础上手动填充对象值是“正确”的做法。
【问题讨论】:
其实我做了更多的调查,问题是工厂女孩。即使我要求它“构建”,它也在其子关联上执行 .create() 。啊!!!!那就是问题所在。问题应该是,“我怎样才能让 Factory Girl 不访问数据库? 好的,我完全改变了我的问题,以符合工厂女孩是真正问题的新事实。 你能告诉我们你的工厂吗? @Douglas F Shearer:当然,我会在问题之前发布它们 @Douglas F Shearer:我的意思是在问题之后;) 【参考方案1】:好吧,我想我会回答我自己的问题。我认为这是正确的答案,也许其他人可以从中学习,因为我必须花费几个小时来学习它。
以下是我获得 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
解决方案就是不使用任何类型的工厂进行控制器测试(可能还有其他类型的测试)。我建议只在非常痛苦的情况下使用 Factory's。
现在所有 3 个测试都在 0.07 秒内运行!在运行所有 3 个测试之前需要 1.4 秒。
Factory_girl 只是一个非常慢的库。我不知道它到底在做什么,但它的配置不正确。
是的,我知道它所做的不仅仅是简单的MyClass.new
语句......但即使对于像 Ruby 这样较慢的脚本语言,其性能也比基本类实例化慢很多数量级。它需要进行一些大规模的优化,以使Factory.build(:my_class)
更符合MyClass.new
我建议 Factory_girl 的实现者尝试获取它,这样它的开销不会比基本的 MyClass.new
调用慢多少(不包括数据库开销......这是无法避免的)。它应该提供一种构建对象的好方法,并且您不必为了获得这种好处而付出 20 倍的性能损失。这不是一个可以接受的权衡。
这真的太糟糕了,因为当您在控制器规格中打开 render_views
时,Factory.build
在控制器中会很好。应该有很大的动力来纠正这个问题。
同时,只需使用基本的 Ruby/Rails 类。我想你会惊讶于它们实际上有多快......
【讨论】:
将存根与任何数据库写入进行比较并不是一个公平的比较。如果您比较工厂女孩所花费的时间,与手动编写 Rails 的model.create
调用所花费的时间,差异很小。您的问题的根源是与build
工厂调用关联的对象的构建策略是创建它们,从而导致数据库调用。解决方案是不使用工厂进行关联。
工厂女孩构建策略的文档:rubydoc.info/gems/factory_girl/1.3.3/file/…
最后一件事......不管工厂的性能如何,存根和模拟都是控制器的选择,使它们成为适当的单元测试。
@Douglas F Shearer:我在我的问题中举了一个例子,它只使用Factory.build()
,它从不接触数据库(通过查看我的 RSpec 日志来验证)。出于所有意图和目的,它几乎与将记录创建到 mysql 中的版本一样慢。访问数据库仅占其成本的 30% 左右。其他 70% 是由于 factory_girl bloat 和 overhead 造成的,您可以完全忽略 factory_girl 的成本,只需不使用它即可。我现在在 5 秒内运行了 250 个控制器测试,几乎没有真正的缺点。
你的设置一定有问题,因为对我来说它几乎没有变慢。 gist.github.com/992170【参考方案2】:
我遇到了与@FireEmblem 相同的问题,最终将问题缩小到FactoryGirl.build
。 FactoryGirl.stub
并没有让事情变得更好。
我终于意识到这是因为我的一个模型具有验证逻辑,当某个字段存在时会发出 HTTP 请求。工厂在那个字段中设置了一个值,所以在外面,看起来 FactoryGirl 正在减慢我的测试速度。实际上,确实如此,但这只是因为它触发了 HTTP 请求。从我的一个工厂中删除一条线消除了 HTTP 请求,导致性能提高了 60 倍。
【讨论】:
以上是关于如果我调用 Factory.build 以使我的控制器测试快速,如何让 Factory Girl 永远不会访问数据库?的主要内容,如果未能解决你的问题,请参考以下文章
如何让我的 CSS @media 代码在移动设备上运行以使我的网站具有响应性?
如何在我的网页中垂直排列水平排列的部分,以使我的页面在设备上响应
需要一些帮助来创建一个 cmake 文件以使我的项目正常工作...从 cmake 转换