jjzjj

ruby-on-rails - 如果我调用 Factory.build 以使我的 Controller 测试快速,我怎样才能让 Factory Girl 永远不会访问数据库?

coder 2025-04-29 原文

我正在寻求使我的 Rails 测试更快。我只有 520 个测试,但它们在 bash 中运行需要 62 秒,在 Rubymine 中运行需要 82 秒。

作为典型 Controller 测试的示例,我使用此代码以@user 身份登录并在 CommentsController 中为我的 RSpec Controller 测试创建基本的@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() 并在我的父 Controller 中清除所有之前的过滤器。然而,当我之后检查 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) block 对测试性能的影响为零。正如我发现的那样,调用 Factory.build() 仍将在内部调用子关联上的 Factory.create()

这是一个 before(:each) block ,它有效地删除了 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 可以获得什么好处——它有很多好处——都不值得付出性能代价。我确信可以开发出性能更高的更好的 factory gem……但遗憾的是,factory_girl 不是。

我下面的解决方案使用了基本的对象实例化和 stub ,并且测试继续通过。我认为在每次测试的基础上使用基本的 Ruby、 stub 和手动填充对象值是“正确”的做法,如果你想避免固定装置并在运行测试时获得高性能的话。

最佳答案

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

以下是我获得 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

解决方案就是不使用任何类型的工厂来进行 Controller 测试(或许还有其他类型的测试)。我建议仅在实在太痛苦而无法使用 Factory 时才使用 Factory。

所有 3 个测试现在都在 0.07 秒内运行!在 1.4 秒之前运行所有 3 个测试。

Factory_girl 只是一个非常慢的库。我不知道它到底在做什么,但它的配置文件不正确。

是的,我知道它做的不仅仅是简单的 MyClass.new 语句...但即使对于像 Ruby 这样较慢的脚本语言,性能也比基本类实例化慢很多数量级.它需要进行一些大规模优化,以便 Factory.build(:my_class)MyClass.new

更加一致

我建议 Factory_girl 的实现者尝试获取它,这样它的开销不会比基本的 MyClass.new 调用慢很多(不包括数据库开销......那不可能避免)。它应该提供一种构建对象的好方法,您不必为了获得此好处而付出 20 倍的性能损失。这不是一个可以接受的权衡。

这一切都太糟糕了,因为当您在 Controller 规范中打开 render_views 时,Factory.build 在 Controller 中会很好。应该有很大的动机来纠正这个问题。

与此同时,只需使用基本的 Ruby/Rails 类。我想您会惊讶于它们实际上有多快....

关于ruby-on-rails - 如果我调用 Factory.build 以使我的 Controller 测试快速,我怎样才能让 Factory Girl 永远不会访问数据库?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6128476/

有关ruby-on-rails - 如果我调用 Factory.build 以使我的 Controller 测试快速,我怎样才能让 Factory Girl 永远不会访问数据库?的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  3. ruby-on-rails - rails : keeping DRY with ActiveRecord models that share similar complex attributes - 2

    这似乎应该有一个直截了当的答案,但在Google上花了很多时间,所以我找不到它。这可能是缺少正确关键字的情况。在我的RoR应用程序中,我有几个模型共享一种特定类型的字符串属性,该属性具有特殊验证和其他功能。我能想到的最接近的类似示例是表示URL的字符串。这会导致模型中出现大量重复(甚至单元测试中会出现更多重复),但我不确定如何让它更DRY。我能想到几个可能的方向...按照“validates_url_format_of”插件,但这只会让验证干给这个特殊的字符串它自己的模型,但这看起来很像重溶液为这个特殊的字符串创建一个ruby​​类,但是我如何得到ActiveRecord关联这个类模型

  4. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  5. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  6. ruby-on-rails - unicode 字符串的长度 - 2

    在我的Rails(2.3,Ruby1.8.7)应用程序中,我需要将字符串截断到一定长度。该字符串是unicode,在控制台中运行测试时,例如'א'.length,我意识到返回了双倍长度。我想要一个与编码无关的长度,以便对unicode字符串或latin1编码字符串进行相同的截断。我已经了解了Ruby的大部分unicode资料,但仍然有些一头雾水。应该如何解决这个问题? 最佳答案 Rails有一个返回多字节字符的mb_chars方法。试试unicode_string.mb_chars.slice(0,50)

  7. ruby-on-rails - Ruby on Rails 迁移,将表更改为 MyISAM - 2

    如何正确创建Rails迁移,以便将表更改为MySQL中的MyISAM?目前是InnoDB。运行原始执行语句会更改表,但它不会更新db/schema.rb,因此当在测试环境中重新创建表时,它会返回到InnoDB并且我的全文搜索失败。我如何着手更改/添加迁移,以便将现有表修改为MyISAM并更新schema.rb,以便我的数据库和相应的测试数据库得到相应更新? 最佳答案 我没有找到执行此操作的好方法。您可以像有人建议的那样更改您的schema.rb,然后运行:rakedb:schema:load,但是,这将覆盖您的数据。我的做法是(假设

  8. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  9. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  10. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

随机推荐